1#![no_std]
19
20pub mod accounts;
21pub mod anchor_idl;
22pub mod clientgen;
23pub mod codama;
24pub mod python_client;
25pub mod rust_client;
26
27use core::fmt;
28use hopper_core::account::HEADER_LEN;
29use hopper_core::field_map::FieldInfo;
30use hopper_runtime::layout::LayoutInfo;
31use hopper_runtime::{AccountView, LayoutContract};
32
33#[cfg(feature = "receipt")]
35pub use hopper_core::receipt::{
36 CompatImpact, DecodedReceipt, NarrativeRisk, Phase, ReceiptExplain, ReceiptNarrative,
37};
38#[cfg(feature = "policy")]
40pub use hopper_core::policy::PolicyClass;
41
42pub const MANIFEST_SEED: &[u8] = b"hopper:manifest";
54
55pub const MANIFEST_MAGIC: [u8; 8] = *b"HOPRMNFT";
57
58pub const MANIFEST_HEADER_LEN: usize = 20;
68
69pub const MANIFEST_VERSION: u32 = 1;
71
72pub const MANIFEST_COMPRESS_NONE: u8 = 0;
74pub const MANIFEST_COMPRESS_ZLIB: u8 = 1;
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82#[repr(u8)]
83pub enum FieldIntent {
84 Balance = 0,
86 Authority = 1,
88 Timestamp = 2,
90 Counter = 3,
92 Index = 4,
94 BasisPoints = 5,
96 Flag = 6,
98 Address = 7,
100 Hash = 8,
102 PDASeed = 9,
104 Version = 10,
106 Bump = 11,
108 Nonce = 12,
110 Supply = 13,
112 Limit = 14,
114 Threshold = 15,
116 Owner = 16,
118 Delegate = 17,
120 Status = 18,
122 Custom = 255,
124}
125
126impl FieldIntent {
127 pub fn name(self) -> &'static str {
129 match self {
130 Self::Balance => "balance",
131 Self::Authority => "authority",
132 Self::Timestamp => "timestamp",
133 Self::Counter => "counter",
134 Self::Index => "index",
135 Self::BasisPoints => "basis_points",
136 Self::Flag => "flag",
137 Self::Address => "address",
138 Self::Hash => "hash",
139 Self::PDASeed => "pda_seed",
140 Self::Version => "version",
141 Self::Bump => "bump",
142 Self::Nonce => "nonce",
143 Self::Supply => "supply",
144 Self::Limit => "limit",
145 Self::Threshold => "threshold",
146 Self::Owner => "owner",
147 Self::Delegate => "delegate",
148 Self::Status => "status",
149 Self::Custom => "custom",
150 }
151 }
152
153 pub fn is_monetary(self) -> bool {
156 matches!(self, Self::Balance | Self::BasisPoints | Self::Supply)
157 }
158
159 pub fn is_identity(self) -> bool {
161 matches!(
162 self,
163 Self::Authority | Self::Address | Self::Owner | Self::Delegate
164 )
165 }
166
167 pub fn is_authority_sensitive(self) -> bool {
169 matches!(self, Self::Authority | Self::Owner | Self::Delegate)
170 }
171
172 pub fn is_init_only(self) -> bool {
174 matches!(self, Self::PDASeed | Self::Bump)
175 }
176
177 pub fn is_governance(self) -> bool {
179 matches!(self, Self::Threshold | Self::Limit | Self::Status)
180 }
181}
182
183impl fmt::Display for FieldIntent {
184 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185 f.write_str(self.name())
186 }
187}
188
189#[derive(Clone, Copy, Debug, PartialEq, Eq)]
198#[repr(u8)]
199pub enum MutationClass {
200 ReadOnly = 0,
202 AppendOnly = 1,
204 InPlace = 2,
206 Resizing = 3,
208 AuthoritySensitive = 4,
210 Financial = 5,
212 StateTransition = 6,
214}
215
216impl MutationClass {
217 pub const fn name(self) -> &'static str {
219 match self {
220 Self::ReadOnly => "read-only",
221 Self::AppendOnly => "append-only",
222 Self::InPlace => "in-place",
223 Self::Resizing => "resizing",
224 Self::AuthoritySensitive => "authority-sensitive",
225 Self::Financial => "financial",
226 Self::StateTransition => "state-transition",
227 }
228 }
229
230 pub const fn is_mutating(self) -> bool {
232 !matches!(self, Self::ReadOnly)
233 }
234
235 pub const fn requires_snapshot(self) -> bool {
237 !matches!(self, Self::ReadOnly)
238 }
239
240 pub const fn requires_authority(self) -> bool {
242 matches!(
243 self,
244 Self::AuthoritySensitive | Self::Financial | Self::Resizing | Self::StateTransition
245 )
246 }
247}
248
249impl fmt::Display for MutationClass {
250 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251 f.write_str(self.name())
252 }
253}
254
255#[derive(Clone, Copy, Debug)]
265pub struct LayoutBehavior {
266 pub requires_signer: bool,
268 pub affects_balance: bool,
270 pub affects_authority: bool,
272 pub mutation_class: MutationClass,
274}
275
276impl LayoutBehavior {
277 pub const READ_ONLY: Self = Self {
279 requires_signer: false,
280 affects_balance: false,
281 affects_authority: false,
282 mutation_class: MutationClass::ReadOnly,
283 };
284
285 pub const STANDARD: Self = Self {
287 requires_signer: true,
288 affects_balance: false,
289 affects_authority: false,
290 mutation_class: MutationClass::InPlace,
291 };
292
293 pub const FINANCIAL: Self = Self {
295 requires_signer: true,
296 affects_balance: true,
297 affects_authority: false,
298 mutation_class: MutationClass::Financial,
299 };
300
301 pub const APPEND_ONLY: Self = Self {
303 requires_signer: true,
304 affects_balance: false,
305 affects_authority: false,
306 mutation_class: MutationClass::AppendOnly,
307 };
308}
309
310impl fmt::Display for LayoutBehavior {
311 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312 write!(f, "mutation={}", self.mutation_class)?;
313 if self.requires_signer {
314 write!(f, " signer")?;
315 }
316 if self.affects_balance {
317 write!(f, " balance")?;
318 }
319 if self.affects_authority {
320 write!(f, " authority")?;
321 }
322 Ok(())
323 }
324}
325
326#[derive(Clone, Copy, Debug, PartialEq, Eq)]
336#[repr(u8)]
337pub enum LayoutStabilityGrade {
338 Stable = 0,
340 Evolving = 1,
342 MigrationSensitive = 2,
344 UnsafeToEvolve = 3,
346}
347
348impl LayoutStabilityGrade {
349 pub const fn name(self) -> &'static str {
351 match self {
352 Self::Stable => "stable",
353 Self::Evolving => "evolving",
354 Self::MigrationSensitive => "migration-sensitive",
355 Self::UnsafeToEvolve => "unsafe-to-evolve",
356 }
357 }
358
359 pub fn compute(manifest: &LayoutManifest) -> Self {
364 let mut authority_count = 0u16;
365 let mut financial_count = 0u16;
366 let mut init_only_count = 0u16;
367 let mut has_custom = false;
368
369 let mut i = 0;
370 while i < manifest.field_count {
371 let intent = manifest.fields[i].intent;
372 if intent.is_authority_sensitive() {
373 authority_count += 1;
374 }
375 if intent.is_monetary() {
376 financial_count += 1;
377 }
378 if intent.is_init_only() {
379 init_only_count += 1;
380 }
381 if matches!(intent, FieldIntent::Custom) {
382 has_custom = true;
383 }
384 i += 1;
385 }
386
387 if authority_count > 2 && financial_count > 2 {
390 return Self::UnsafeToEvolve;
391 }
392 if authority_count > 1 || financial_count > 2 {
393 return Self::MigrationSensitive;
394 }
395 if has_custom && manifest.field_count > 8 {
396 return Self::Evolving;
397 }
398 if init_only_count > 0 {
401 return Self::Stable;
402 }
403 Self::Evolving
404 }
405}
406
407impl fmt::Display for LayoutStabilityGrade {
408 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409 f.write_str(self.name())
410 }
411}
412
413#[derive(Clone, Copy, Debug)]
415pub struct FieldDescriptor {
416 pub name: &'static str,
418 pub canonical_type: &'static str,
420 pub size: u16,
422 pub offset: u16,
424 pub intent: FieldIntent,
426}
427
428#[derive(Clone, Copy, Debug)]
430pub struct LayoutManifest {
431 pub name: &'static str,
433 pub disc: u8,
435 pub version: u8,
437 pub layout_id: [u8; 8],
439 pub total_size: usize,
441 pub field_count: usize,
443 pub fields: &'static [FieldDescriptor],
445}
446
447#[derive(Clone, Copy, Debug, PartialEq, Eq)]
458pub struct LayoutFingerprint {
459 pub wire_hash: [u8; 8],
461 pub semantic_hash: [u8; 8],
463}
464
465impl LayoutFingerprint {
466 pub const fn from_manifest(manifest: &LayoutManifest) -> Self {
472 Self {
473 wire_hash: manifest.layout_id,
474 semantic_hash: Self::compute_semantic(manifest.fields),
475 }
476 }
477
478 pub const fn is_identical(&self, other: &Self) -> bool {
480 let mut i = 0;
481 while i < 8 {
482 if self.wire_hash[i] != other.wire_hash[i] {
483 return false;
484 }
485 if self.semantic_hash[i] != other.semantic_hash[i] {
486 return false;
487 }
488 i += 1;
489 }
490 true
491 }
492
493 pub const fn wire_matches_but_semantics_differ(&self, other: &Self) -> bool {
497 let mut wire_eq = true;
498 let mut sem_eq = true;
499 let mut i = 0;
500 while i < 8 {
501 if self.wire_hash[i] != other.wire_hash[i] {
502 wire_eq = false;
503 }
504 if self.semantic_hash[i] != other.semantic_hash[i] {
505 sem_eq = false;
506 }
507 i += 1;
508 }
509 wire_eq && !sem_eq
510 }
511
512 const fn compute_semantic(fields: &[FieldDescriptor]) -> [u8; 8] {
514 const FNV_OFFSET: u64 = 0xcbf29ce484222325;
515 const FNV_PRIME: u64 = 0x00000100000001B3;
516
517 let mut hash = FNV_OFFSET;
518 let mut i = 0;
519 while i < fields.len() {
520 let name = fields[i].name.as_bytes();
522 let mut j = 0;
523 while j < name.len() {
524 hash ^= name[j] as u64;
525 hash = hash.wrapping_mul(FNV_PRIME);
526 j += 1;
527 }
528 let ty = fields[i].canonical_type.as_bytes();
530 j = 0;
531 while j < ty.len() {
532 hash ^= ty[j] as u64;
533 hash = hash.wrapping_mul(FNV_PRIME);
534 j += 1;
535 }
536 hash ^= fields[i].size as u64;
538 hash = hash.wrapping_mul(FNV_PRIME);
539 hash ^= fields[i].offset as u64;
540 hash = hash.wrapping_mul(FNV_PRIME);
541 hash ^= fields[i].intent as u8 as u64;
543 hash = hash.wrapping_mul(FNV_PRIME);
544 i += 1;
545 }
546 hash.to_le_bytes()
547 }
548}
549
550impl fmt::Display for LayoutFingerprint {
551 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
552 write!(f, "wire=")?;
553 let mut i = 0;
554 while i < 8 {
555 let _ = write!(f, "{:02x}", self.wire_hash[i]);
556 i += 1;
557 }
558 write!(f, " sem=")?;
559 i = 0;
560 while i < 8 {
561 let _ = write!(f, "{:02x}", self.semantic_hash[i]);
562 i += 1;
563 }
564 Ok(())
565 }
566}
567
568#[inline]
578pub fn is_append_compatible(older: &LayoutManifest, newer: &LayoutManifest) -> bool {
579 older.disc == newer.disc
580 && newer.version > older.version
581 && newer.total_size >= older.total_size
582 && older.layout_id != newer.layout_id
583}
584
585#[inline]
591pub fn requires_migration(older: &LayoutManifest, newer: &LayoutManifest) -> bool {
592 older.disc == newer.disc && older.layout_id != newer.layout_id
593}
594
595#[inline]
610pub fn is_backward_readable(older: &LayoutManifest, newer: &LayoutManifest) -> bool {
611 if older.disc != newer.disc {
612 return false;
613 }
614 if newer.field_count < older.field_count {
616 return false;
617 }
618 let mut i = 0;
619 while i < older.field_count {
620 let old_f = &older.fields[i];
621 if i >= newer.field_count {
623 return false;
624 }
625 let new_f = &newer.fields[i];
626 if !const_str_eq(old_f.name, new_f.name)
627 || !const_str_eq(old_f.canonical_type, new_f.canonical_type)
628 || old_f.size != new_f.size
629 {
630 return false;
631 }
632 i += 1;
633 }
634 true
636}
637
638#[derive(Clone, Copy, Debug, PartialEq, Eq)]
645pub enum CompatibilityVerdict {
646 Identical,
648 WireCompatible,
652 AppendSafe,
657 MigrationRequired,
660 Incompatible,
662}
663
664impl CompatibilityVerdict {
665 #[inline]
667 pub fn between(older: &LayoutManifest, newer: &LayoutManifest) -> Self {
668 if older.layout_id == newer.layout_id {
669 return Self::Identical;
670 }
671 if older.disc != newer.disc {
672 return Self::Incompatible;
673 }
674 let backward = is_backward_readable(older, newer);
675 if backward
678 && older.field_count == newer.field_count
679 && older.total_size == newer.total_size
680 {
681 return Self::WireCompatible;
682 }
683 if backward {
684 Self::AppendSafe
685 } else {
686 Self::MigrationRequired
687 }
688 }
689
690 #[inline]
692 pub const fn name(self) -> &'static str {
693 match self {
694 Self::Identical => "identical",
695 Self::WireCompatible => "wire-compatible",
696 Self::AppendSafe => "append-safe",
697 Self::MigrationRequired => "migration-required",
698 Self::Incompatible => "incompatible",
699 }
700 }
701
702 #[inline]
704 pub const fn is_safe(self) -> bool {
705 matches!(
706 self,
707 Self::Identical | Self::WireCompatible | Self::AppendSafe
708 )
709 }
710
711 #[inline]
713 pub const fn is_backward_readable(self) -> bool {
714 matches!(
715 self,
716 Self::Identical | Self::WireCompatible | Self::AppendSafe
717 )
718 }
719
720 #[inline]
722 pub const fn requires_migration(self) -> bool {
723 matches!(self, Self::MigrationRequired)
724 }
725
726 pub fn refine_with_roles<const N: usize>(self, report: &SegmentMigrationReport<N>) -> Self {
738 match self {
739 Self::MigrationRequired => {
740 let mut i = 0;
743 let mut all_soft = true;
744 while i < report.count {
745 let adv = &report.advice[i];
746 if adv.must_preserve && !adv.clearable && !adv.rebuildable {
747 all_soft = false;
749 break;
750 }
751 i += 1;
752 }
753 if all_soft && report.count > 0 {
754 Self::AppendSafe
755 } else {
756 self
757 }
758 }
759 Self::AppendSafe => {
760 let mut i = 0;
762 while i < report.count {
763 if report.advice[i].immutable {
764 return Self::MigrationRequired;
765 }
766 i += 1;
767 }
768 self
769 }
770 _ => self,
771 }
772 }
773}
774
775impl fmt::Display for CompatibilityVerdict {
776 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
777 f.write_str(self.name())
778 }
779}
780
781pub struct CompatibilityExplain {
790 pub verdict: CompatibilityVerdict,
792 pub added_fields: [&'static str; 16],
794 pub added_count: u8,
796 pub removed_fields: [&'static str; 16],
798 pub removed_count: u8,
800 pub changed_fields: [&'static str; 16],
802 pub changed_count: u8,
804 pub semantic_drift: bool,
806 pub summary: &'static str,
808}
809
810impl CompatibilityExplain {
811 pub fn between(older: &LayoutManifest, newer: &LayoutManifest) -> Self {
813 let verdict = CompatibilityVerdict::between(older, newer);
814
815 let mut added = [""; 16];
816 let mut added_n = 0u8;
817 let mut removed = [""; 16];
818 let mut removed_n = 0u8;
819 let mut changed = [""; 16];
820 let mut changed_n = 0u8;
821
822 let shared = if older.field_count < newer.field_count {
824 older.field_count
825 } else {
826 newer.field_count
827 };
828
829 let mut i = 0;
830 while i < shared {
831 let old_f = &older.fields[i];
832 let new_f = &newer.fields[i];
833 let name_eq = const_str_eq(old_f.name, new_f.name);
834 let type_eq = const_str_eq(old_f.canonical_type, new_f.canonical_type);
835 let size_eq = old_f.size == new_f.size;
836 if !(name_eq && type_eq && size_eq) {
837 if (changed_n as usize) < 16 {
838 changed[changed_n as usize] = old_f.name;
839 changed_n += 1;
840 }
841 }
842 i += 1;
843 }
844 while i < newer.field_count {
846 if (added_n as usize) < 16 {
847 added[added_n as usize] = newer.fields[i].name;
848 added_n += 1;
849 }
850 i += 1;
851 }
852 let mut j = shared;
854 while j < older.field_count {
855 if (removed_n as usize) < 16 {
856 removed[removed_n as usize] = older.fields[j].name;
857 removed_n += 1;
858 }
859 j += 1;
860 }
861
862 let fp_old = LayoutFingerprint::from_manifest(older);
863 let fp_new = LayoutFingerprint::from_manifest(newer);
864 let semantic_drift = fp_old.wire_matches_but_semantics_differ(&fp_new);
865
866 let summary = match verdict {
867 CompatibilityVerdict::Identical => "Layouts are byte-identical. No action needed.",
868 CompatibilityVerdict::WireCompatible => {
869 if semantic_drift {
870 "Wire layout matches but field semantics changed. Review field intents."
871 } else {
872 "Wire layout matches with metadata-only changes. Safe to deploy."
873 }
874 }
875 CompatibilityVerdict::AppendSafe => {
876 "New fields appended at the end. Old readers still work."
877 }
878 CompatibilityVerdict::MigrationRequired => {
879 "Breaking field changes. Migration instruction required before deploy."
880 }
881 CompatibilityVerdict::Incompatible => {
882 "Different discriminators. These are unrelated account types."
883 }
884 };
885
886 Self {
887 verdict,
888 added_fields: added,
889 added_count: added_n,
890 removed_fields: removed,
891 removed_count: removed_n,
892 changed_fields: changed,
893 changed_count: changed_n,
894 semantic_drift,
895 summary,
896 }
897 }
898}
899
900impl fmt::Display for CompatibilityExplain {
901 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
902 writeln!(f, "Verdict: {} ({})", self.verdict.name(), self.summary)?;
903 if self.added_count > 0 {
904 write!(f, " Added:")?;
905 let mut i = 0;
906 while i < self.added_count as usize {
907 write!(f, " {}", self.added_fields[i])?;
908 i += 1;
909 }
910 writeln!(f)?;
911 }
912 if self.removed_count > 0 {
913 write!(f, " Removed:")?;
914 let mut i = 0;
915 while i < self.removed_count as usize {
916 write!(f, " {}", self.removed_fields[i])?;
917 i += 1;
918 }
919 writeln!(f)?;
920 }
921 if self.changed_count > 0 {
922 write!(f, " Changed:")?;
923 let mut i = 0;
924 while i < self.changed_count as usize {
925 write!(f, " {}", self.changed_fields[i])?;
926 i += 1;
927 }
928 writeln!(f)?;
929 }
930 if self.semantic_drift {
931 writeln!(
932 f,
933 " Warning: semantic drift detected (wire matches but meaning changed)"
934 )?;
935 }
936 Ok(())
937 }
938}
939
940#[derive(Clone, Copy, PartialEq, Eq, Debug)]
942pub enum FieldCompat {
943 Identical,
945 Changed,
947 Added,
949 Removed,
951}
952
953#[inline]
959pub fn compare_fields<'a, const N: usize>(
960 older: &'a LayoutManifest,
961 newer: &'a LayoutManifest,
962) -> FieldCompatReport<'a, N> {
963 let mut report = FieldCompatReport {
964 entries: [FieldCompatEntry {
965 name: "",
966 status: FieldCompat::Identical,
967 }; N],
968 count: 0,
969 is_append_safe: true,
970 };
971
972 let shared = if older.field_count < newer.field_count {
974 older.field_count
975 } else {
976 newer.field_count
977 };
978
979 let mut i = 0;
980 while i < shared && report.count < N {
981 let old_f = &older.fields[i];
982 let new_f = &newer.fields[i];
983
984 let name_eq = const_str_eq(old_f.name, new_f.name);
985 let type_eq = const_str_eq(old_f.canonical_type, new_f.canonical_type);
986 let size_eq = old_f.size == new_f.size;
987
988 let status = if name_eq && type_eq && size_eq {
989 FieldCompat::Identical
990 } else {
991 report.is_append_safe = false;
992 FieldCompat::Changed
993 };
994
995 report.entries[report.count] = FieldCompatEntry {
996 name: old_f.name,
997 status,
998 };
999 report.count += 1;
1000 i += 1;
1001 }
1002
1003 while i < newer.field_count && report.count < N {
1005 report.entries[report.count] = FieldCompatEntry {
1006 name: newer.fields[i].name,
1007 status: FieldCompat::Added,
1008 };
1009 report.count += 1;
1010 i += 1;
1011 }
1012
1013 let mut j = shared;
1015 while j < older.field_count && report.count < N {
1016 report.entries[report.count] = FieldCompatEntry {
1017 name: older.fields[j].name,
1018 status: FieldCompat::Removed,
1019 };
1020 report.count += 1;
1021 report.is_append_safe = false;
1022 j += 1;
1023 }
1024
1025 report
1026}
1027
1028#[derive(Clone, Copy)]
1030pub struct FieldCompatEntry<'a> {
1031 pub name: &'a str,
1032 pub status: FieldCompat,
1033}
1034
1035pub struct FieldCompatReport<'a, const N: usize> {
1037 pub entries: [FieldCompatEntry<'a>; N],
1038 pub count: usize,
1039 pub is_append_safe: bool,
1041}
1042
1043impl<'a, const N: usize> FieldCompatReport<'a, N> {
1044 #[inline(always)]
1046 pub fn len(&self) -> usize {
1047 self.count
1048 }
1049
1050 #[inline(always)]
1052 pub fn is_empty(&self) -> bool {
1053 self.count == 0
1054 }
1055
1056 #[inline(always)]
1058 pub fn get(&self, i: usize) -> Option<&FieldCompatEntry<'a>> {
1059 if i < self.count {
1060 Some(&self.entries[i])
1061 } else {
1062 None
1063 }
1064 }
1065
1066 #[inline(always)]
1068 pub fn is_append_safe(&self) -> bool {
1069 self.is_append_safe
1070 }
1071
1072 #[inline]
1074 pub fn count_status(&self, status: FieldCompat) -> usize {
1075 let mut n = 0;
1076 let mut i = 0;
1077 while i < self.count {
1078 if self.entries[i].status == status {
1079 n += 1;
1080 }
1081 i += 1;
1082 }
1083 n
1084 }
1085}
1086
1087#[derive(Clone, Copy)]
1091pub struct DecodedHeader {
1092 pub disc: u8,
1093 pub version: u8,
1094 pub flags: u16,
1095 pub layout_id: [u8; 8],
1096 pub reserved: [u8; 4],
1097}
1098
1099#[inline]
1104pub fn decode_header(data: &[u8]) -> Option<DecodedHeader> {
1105 if data.len() < HEADER_LEN {
1106 return None;
1107 }
1108 Some(DecodedHeader {
1109 disc: data[0],
1110 version: data[1],
1111 flags: u16::from_le_bytes([data[2], data[3]]),
1112 layout_id: [
1113 data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
1114 ],
1115 reserved: [data[12], data[13], data[14], data[15]],
1116 })
1117}
1118
1119#[inline]
1124pub fn identify_account<'a>(
1125 data: &[u8],
1126 manifests: &'a [LayoutManifest],
1127) -> Option<(usize, &'a LayoutManifest)> {
1128 let header = decode_header(data)?;
1129 let mut i = 0;
1130 while i < manifests.len() {
1131 let m = &manifests[i];
1132 if m.disc == header.disc && m.layout_id == header.layout_id {
1133 return Some((i, m));
1134 }
1135 i += 1;
1136 }
1137 None
1138}
1139
1140#[derive(Clone, Copy)]
1144pub struct DecodedSegment {
1145 pub id: [u8; 4],
1146 pub offset: u32,
1147 pub size: u32,
1148 pub flags: u16,
1149 pub version: u8,
1150}
1151
1152#[inline]
1157pub fn decode_segments<const N: usize>(data: &[u8]) -> Option<(usize, [DecodedSegment; N])> {
1158 let registry_start = HEADER_LEN;
1159 if data.len() < registry_start + 4 {
1160 return None;
1161 }
1162
1163 let count = u16::from_le_bytes([data[registry_start], data[registry_start + 1]]) as usize;
1164 if count > N {
1165 return None;
1166 }
1167
1168 let entries_start = registry_start + 4;
1169 let mut segments = [DecodedSegment {
1170 id: [0; 4],
1171 offset: 0,
1172 size: 0,
1173 flags: 0,
1174 version: 0,
1175 }; N];
1176
1177 let mut i = 0;
1178 while i < count {
1179 let off = entries_start + i * 16;
1180 if off + 16 > data.len() {
1181 return None;
1182 }
1183 segments[i] = DecodedSegment {
1184 id: [data[off], data[off + 1], data[off + 2], data[off + 3]],
1185 offset: u32::from_le_bytes([
1186 data[off + 4],
1187 data[off + 5],
1188 data[off + 6],
1189 data[off + 7],
1190 ]),
1191 size: u32::from_le_bytes([
1192 data[off + 8],
1193 data[off + 9],
1194 data[off + 10],
1195 data[off + 11],
1196 ]),
1197 flags: u16::from_le_bytes([data[off + 12], data[off + 13]]),
1198 version: data[off + 14],
1199 };
1200 i += 1;
1201 }
1202
1203 Some((count, segments))
1204}
1205
1206pub struct ManifestRegistry<const N: usize> {
1225 manifests: [Option<LayoutManifest>; N],
1226 count: usize,
1227}
1228
1229impl<const N: usize> ManifestRegistry<N> {
1230 #[inline(always)]
1232 pub const fn empty() -> Self {
1233 Self {
1234 manifests: [None; N],
1235 count: 0,
1236 }
1237 }
1238
1239 #[inline]
1241 pub const fn from_slice(manifests: &[LayoutManifest]) -> Self {
1242 let mut reg = Self::empty();
1243 let mut i = 0;
1244 while i < manifests.len() && i < N {
1245 reg.manifests[i] = Some(manifests[i]);
1246 reg.count += 1;
1247 i += 1;
1248 }
1249 reg
1250 }
1251
1252 #[inline(always)]
1254 pub const fn len(&self) -> usize {
1255 self.count
1256 }
1257
1258 #[inline(always)]
1260 pub const fn is_empty(&self) -> bool {
1261 self.count == 0
1262 }
1263
1264 #[inline]
1266 pub fn identify(&self, data: &[u8]) -> Option<(usize, &LayoutManifest)> {
1267 let header = decode_header(data)?;
1268 let mut i = 0;
1269 while i < self.count {
1270 if let Some(m) = &self.manifests[i] {
1271 if m.disc == header.disc && m.layout_id == header.layout_id {
1272 return Some((i, m));
1273 }
1274 }
1275 i += 1;
1276 }
1277 None
1278 }
1279
1280 #[inline]
1282 pub fn get(&self, index: usize) -> Option<&LayoutManifest> {
1283 if index < self.count {
1284 self.manifests[index].as_ref()
1285 } else {
1286 None
1287 }
1288 }
1289
1290 #[inline]
1292 pub fn find_by_disc(&self, disc: u8) -> Option<&LayoutManifest> {
1293 let mut i = 0;
1294 while i < self.count {
1295 if let Some(m) = &self.manifests[i] {
1296 if m.disc == disc {
1297 return Some(m);
1298 }
1299 }
1300 i += 1;
1301 }
1302 None
1303 }
1304
1305 #[inline]
1307 pub fn find_by_layout_id(&self, layout_id: &[u8; 8]) -> Option<&LayoutManifest> {
1308 let mut i = 0;
1309 while i < self.count {
1310 if let Some(m) = &self.manifests[i] {
1311 if &m.layout_id == layout_id {
1312 return Some(m);
1313 }
1314 }
1315 i += 1;
1316 }
1317 None
1318 }
1319}
1320
1321#[inline]
1325fn const_str_eq(a: &str, b: &str) -> bool {
1326 let a = a.as_bytes();
1327 let b = b.as_bytes();
1328 if a.len() != b.len() {
1329 return false;
1330 }
1331 let mut i = 0;
1332 while i < a.len() {
1333 if a[i] != b[i] {
1334 return false;
1335 }
1336 i += 1;
1337 }
1338 true
1339}
1340
1341#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1345pub enum MigrationPolicy {
1346 NoOp,
1348 AppendOnly,
1351 RequiresMigration,
1354 Incompatible,
1356}
1357
1358#[derive(Clone, Copy)]
1360pub struct MigrationStep<'a> {
1361 pub action: MigrationAction,
1363 pub field: &'a str,
1365 pub offset: u16,
1367 pub size: u16,
1369}
1370
1371#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1373pub enum MigrationAction {
1374 CopyPrefix,
1376 ZeroInit,
1378 UpdateHeader,
1380 Realloc,
1382}
1383
1384pub struct MigrationPlan<'a, const N: usize> {
1389 pub policy: MigrationPolicy,
1390 pub steps: [MigrationStep<'a>; N],
1391 pub step_count: usize,
1392 pub old_size: usize,
1394 pub new_size: usize,
1396 pub copy_bytes: usize,
1398 pub zero_bytes: usize,
1400 pub backward_readable: bool,
1402}
1403
1404impl<'a, const N: usize> MigrationPlan<'a, N> {
1405 pub fn generate(older: &'a LayoutManifest, newer: &'a LayoutManifest) -> Self {
1410 let mut plan = Self {
1411 policy: MigrationPolicy::NoOp,
1412 steps: [MigrationStep {
1413 action: MigrationAction::CopyPrefix,
1414 field: "",
1415 offset: 0,
1416 size: 0,
1417 }; N],
1418 step_count: 0,
1419 old_size: older.total_size,
1420 new_size: newer.total_size,
1421 copy_bytes: 0,
1422 zero_bytes: 0,
1423 backward_readable: is_backward_readable(older, newer),
1424 };
1425
1426 if older.layout_id == newer.layout_id {
1428 plan.policy = MigrationPolicy::NoOp;
1429 return plan;
1430 }
1431
1432 if older.disc != newer.disc {
1434 plan.policy = MigrationPolicy::Incompatible;
1435 return plan;
1436 }
1437
1438 let report = compare_fields::<32>(older, newer);
1440
1441 if !report.is_append_safe {
1442 plan.policy = MigrationPolicy::RequiresMigration;
1443 } else {
1444 plan.policy = MigrationPolicy::AppendOnly;
1445 }
1446
1447 let mut compatible_end: u16 = HEADER_LEN as u16;
1449 let mut i = 0;
1450 while i < report.count {
1451 if report.entries[i].status == FieldCompat::Identical {
1452 let mut j = 0;
1454 while j < older.field_count {
1455 if const_str_eq(older.fields[j].name, report.entries[i].name) {
1456 let field_end = older.fields[j].offset + older.fields[j].size;
1457 if field_end > compatible_end {
1458 compatible_end = field_end;
1459 }
1460 break;
1461 }
1462 j += 1;
1463 }
1464 }
1465 i += 1;
1466 }
1467
1468 if compatible_end > HEADER_LEN as u16 && plan.step_count < N {
1469 let copy_size = compatible_end - HEADER_LEN as u16;
1470 plan.steps[plan.step_count] = MigrationStep {
1471 action: MigrationAction::CopyPrefix,
1472 field: "",
1473 offset: HEADER_LEN as u16,
1474 size: copy_size,
1475 };
1476 plan.copy_bytes = copy_size as usize;
1477 plan.step_count += 1;
1478 }
1479
1480 if newer.total_size != older.total_size && plan.step_count < N {
1482 plan.steps[plan.step_count] = MigrationStep {
1483 action: MigrationAction::Realloc,
1484 field: "",
1485 offset: 0,
1486 size: newer.total_size as u16,
1487 };
1488 plan.step_count += 1;
1489 }
1490
1491 i = 0;
1493 while i < report.count {
1494 if report.entries[i].status == FieldCompat::Added && plan.step_count < N {
1495 let mut j = 0;
1497 while j < newer.field_count {
1498 if const_str_eq(newer.fields[j].name, report.entries[i].name) {
1499 plan.steps[plan.step_count] = MigrationStep {
1500 action: MigrationAction::ZeroInit,
1501 field: report.entries[i].name,
1502 offset: newer.fields[j].offset,
1503 size: newer.fields[j].size,
1504 };
1505 plan.zero_bytes += newer.fields[j].size as usize;
1506 plan.step_count += 1;
1507 break;
1508 }
1509 j += 1;
1510 }
1511 }
1512 i += 1;
1513 }
1514
1515 if plan.step_count < N {
1517 plan.steps[plan.step_count] = MigrationStep {
1518 action: MigrationAction::UpdateHeader,
1519 field: "",
1520 offset: 0,
1521 size: HEADER_LEN as u16,
1522 };
1523 plan.step_count += 1;
1524 }
1525
1526 plan
1527 }
1528
1529 #[inline(always)]
1531 pub fn len(&self) -> usize {
1532 self.step_count
1533 }
1534
1535 #[inline(always)]
1537 pub fn is_empty(&self) -> bool {
1538 self.step_count == 0
1539 }
1540
1541 #[inline(always)]
1543 pub fn requires_data_copy(&self) -> bool {
1544 self.policy == MigrationPolicy::RequiresMigration
1545 }
1546
1547 #[inline(always)]
1549 pub fn step(&self, i: usize) -> Option<&MigrationStep<'a>> {
1550 if i < self.step_count {
1551 Some(&self.steps[i])
1552 } else {
1553 None
1554 }
1555 }
1556
1557 #[inline]
1559 pub fn for_each_step<F: FnMut(usize, &MigrationStep<'a>)>(&self, mut f: F) {
1560 let mut i = 0;
1561 while i < self.step_count {
1562 f(i, &self.steps[i]);
1563 i += 1;
1564 }
1565 }
1566}
1567
1568#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1575#[repr(u8)]
1576pub enum SegmentRoleHint {
1577 Core = 0,
1578 Extension = 1,
1579 Journal = 2,
1580 Index = 3,
1581 Cache = 4,
1582 Audit = 5,
1583 Shard = 6,
1584 Unclassified = 7,
1585}
1586
1587impl SegmentRoleHint {
1588 #[inline(always)]
1590 pub fn from_flags(flags: u16) -> Self {
1591 match (flags >> 12) & 0x0F {
1592 0 => Self::Core,
1593 1 => Self::Extension,
1594 2 => Self::Journal,
1595 3 => Self::Index,
1596 4 => Self::Cache,
1597 5 => Self::Audit,
1598 6 => Self::Shard,
1599 _ => Self::Unclassified,
1600 }
1601 }
1602
1603 #[inline(always)]
1605 pub fn name(self) -> &'static str {
1606 match self {
1607 Self::Core => "Core",
1608 Self::Extension => "Extension",
1609 Self::Journal => "Journal",
1610 Self::Index => "Index",
1611 Self::Cache => "Cache",
1612 Self::Audit => "Audit",
1613 Self::Shard => "Shard",
1614 Self::Unclassified => "Unclassified",
1615 }
1616 }
1617
1618 #[inline(always)]
1620 pub fn must_preserve(self) -> bool {
1621 matches!(
1622 self,
1623 Self::Core | Self::Extension | Self::Audit | Self::Shard
1624 )
1625 }
1626
1627 #[inline(always)]
1629 pub fn is_rebuildable(self) -> bool {
1630 matches!(self, Self::Index | Self::Cache)
1631 }
1632
1633 #[inline(always)]
1635 pub fn is_clearable(self) -> bool {
1636 matches!(self, Self::Journal | Self::Index | Self::Cache)
1637 }
1638
1639 #[inline(always)]
1641 pub fn is_append_only(self) -> bool {
1642 matches!(self, Self::Journal | Self::Audit)
1643 }
1644
1645 #[inline(always)]
1647 pub fn is_immutable(self) -> bool {
1648 matches!(self, Self::Audit)
1649 }
1650
1651 #[inline(always)]
1656 pub fn requires_migration_copy(self) -> bool {
1657 matches!(self, Self::Core | Self::Audit)
1658 }
1659
1660 #[inline(always)]
1665 pub fn is_safe_to_drop(self) -> bool {
1666 matches!(self, Self::Cache)
1667 }
1668}
1669
1670#[derive(Clone, Copy)]
1672pub struct SegmentAdvice {
1673 pub id: [u8; 4],
1675 pub size: u32,
1677 pub role: SegmentRoleHint,
1679 pub must_preserve: bool,
1681 pub clearable: bool,
1683 pub rebuildable: bool,
1685 pub append_only: bool,
1687 pub immutable: bool,
1689}
1690
1691pub struct SegmentMigrationReport<const N: usize> {
1697 pub advice: [SegmentAdvice; N],
1698 pub count: usize,
1699 pub preserve_bytes: u32,
1701 pub clearable_bytes: u32,
1703 pub rebuildable_bytes: u32,
1705}
1706
1707impl<const N: usize> SegmentMigrationReport<N> {
1708 pub fn analyze(segments: &[DecodedSegment], count: usize) -> Self {
1710 let mut report = Self {
1711 advice: [SegmentAdvice {
1712 id: [0; 4],
1713 size: 0,
1714 role: SegmentRoleHint::Unclassified,
1715 must_preserve: false,
1716 clearable: false,
1717 rebuildable: false,
1718 append_only: false,
1719 immutable: false,
1720 }; N],
1721 count: 0,
1722 preserve_bytes: 0,
1723 clearable_bytes: 0,
1724 rebuildable_bytes: 0,
1725 };
1726
1727 let mut i = 0;
1728 while i < count && i < N {
1729 let seg = &segments[i];
1730 let role = SegmentRoleHint::from_flags(seg.flags);
1731
1732 report.advice[i] = SegmentAdvice {
1733 id: seg.id,
1734 size: seg.size,
1735 role,
1736 must_preserve: role.must_preserve(),
1737 clearable: role.is_clearable(),
1738 rebuildable: role.is_rebuildable(),
1739 append_only: role.is_append_only(),
1740 immutable: role.is_immutable(),
1741 };
1742
1743 if role.must_preserve() {
1744 report.preserve_bytes += seg.size;
1745 }
1746 if role.is_clearable() {
1747 report.clearable_bytes += seg.size;
1748 }
1749 if role.is_rebuildable() {
1750 report.rebuildable_bytes += seg.size;
1751 }
1752
1753 report.count += 1;
1754 i += 1;
1755 }
1756
1757 report
1758 }
1759
1760 pub fn must_preserve_count(&self) -> usize {
1762 let mut n = 0;
1763 let mut i = 0;
1764 while i < self.count {
1765 if self.advice[i].must_preserve {
1766 n += 1;
1767 }
1768 i += 1;
1769 }
1770 n
1771 }
1772
1773 pub fn clearable_count(&self) -> usize {
1775 let mut n = 0;
1776 let mut i = 0;
1777 while i < self.count {
1778 if self.advice[i].clearable {
1779 n += 1;
1780 }
1781 i += 1;
1782 }
1783 n
1784 }
1785}
1786
1787impl<const N: usize> fmt::Display for SegmentMigrationReport<N> {
1788 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1789 writeln!(f, "Segment Migration Advice ({} segments):", self.count)?;
1790 let mut i = 0;
1791 while i < self.count {
1792 let a = &self.advice[i];
1793 write!(f, " [{}] {} ({} bytes):", i, a.role.name(), a.size)?;
1794 if a.must_preserve {
1795 write!(f, " MUST-PRESERVE")?;
1796 }
1797 if a.clearable {
1798 write!(f, " clearable")?;
1799 }
1800 if a.rebuildable {
1801 write!(f, " rebuildable")?;
1802 }
1803 if a.append_only {
1804 write!(f, " append-only")?;
1805 }
1806 if a.immutable {
1807 write!(f, " immutable")?;
1808 }
1809 writeln!(f)?;
1810 i += 1;
1811 }
1812 writeln!(
1813 f,
1814 " preserve={} bytes, clearable={} bytes, rebuildable={} bytes",
1815 self.preserve_bytes, self.clearable_bytes, self.rebuildable_bytes
1816 )?;
1817 Ok(())
1818 }
1819}
1820
1821impl fmt::Display for DecodedHeader {
1828 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1829 write!(
1830 f,
1831 "Header {{ disc: {}, ver: {}, flags: 0x{:04x}, layout_id: ",
1832 self.disc, self.version, self.flags,
1833 )?;
1834 write_hex(f, &self.layout_id)?;
1835 write!(f, " }}")
1836 }
1837}
1838
1839impl fmt::Debug for DecodedHeader {
1840 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1841 write!(
1842 f,
1843 "DecodedHeader {{ disc: {}, version: {}, flags: 0x{:04x}, layout_id: ",
1844 self.disc, self.version, self.flags,
1845 )?;
1846 write_hex(f, &self.layout_id)?;
1847 write!(f, ", reserved: ")?;
1848 write_hex(f, &self.reserved)?;
1849 write!(f, " }}")
1850 }
1851}
1852
1853impl fmt::Display for DecodedSegment {
1854 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1855 write!(f, "Segment {{ id: ")?;
1856 write_hex(f, &self.id)?;
1857 write!(
1858 f,
1859 ", offset: {}, size: {}, flags: 0x{:04x}, ver: {} }}",
1860 self.offset, self.size, self.flags, self.version,
1861 )
1862 }
1863}
1864
1865impl fmt::Debug for DecodedSegment {
1866 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1867 write!(f, "DecodedSegment {{ id: ")?;
1868 write_hex(f, &self.id)?;
1869 write!(
1870 f,
1871 ", offset: {}, size: {}, flags: 0x{:04x}, version: {} }}",
1872 self.offset, self.size, self.flags, self.version,
1873 )
1874 }
1875}
1876
1877impl fmt::Display for FieldCompat {
1878 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1879 match self {
1880 FieldCompat::Identical => write!(f, "identical"),
1881 FieldCompat::Changed => write!(f, "changed"),
1882 FieldCompat::Added => write!(f, "added"),
1883 FieldCompat::Removed => write!(f, "removed"),
1884 }
1885 }
1886}
1887
1888impl fmt::Display for MigrationPolicy {
1889 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1890 match self {
1891 MigrationPolicy::NoOp => write!(f, "no-op"),
1892 MigrationPolicy::AppendOnly => write!(f, "append-only"),
1893 MigrationPolicy::RequiresMigration => write!(f, "requires-migration"),
1894 MigrationPolicy::Incompatible => write!(f, "incompatible"),
1895 }
1896 }
1897}
1898
1899impl fmt::Display for MigrationAction {
1900 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1901 match self {
1902 MigrationAction::CopyPrefix => write!(f, "copy-prefix"),
1903 MigrationAction::ZeroInit => write!(f, "zero-init"),
1904 MigrationAction::UpdateHeader => write!(f, "update-header"),
1905 MigrationAction::Realloc => write!(f, "realloc"),
1906 }
1907 }
1908}
1909
1910impl<'a> fmt::Display for MigrationStep<'a> {
1911 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1912 write!(
1913 f,
1914 "{} @ offset={}, size={}",
1915 self.action, self.offset, self.size
1916 )?;
1917 if !self.field.is_empty() {
1918 write!(f, " (field: {})", self.field)?;
1919 }
1920 Ok(())
1921 }
1922}
1923
1924impl<'a, const N: usize> fmt::Display for MigrationPlan<'a, N> {
1925 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1926 writeln!(f, "MigrationPlan ({}):", self.policy)?;
1927 writeln!(
1928 f,
1929 " old_size={}, new_size={}",
1930 self.old_size, self.new_size
1931 )?;
1932 writeln!(
1933 f,
1934 " copy={} bytes, zero={} bytes",
1935 self.copy_bytes, self.zero_bytes
1936 )?;
1937 let mut i = 0;
1938 while i < self.step_count {
1939 writeln!(f, " step {}: {}", i, self.steps[i])?;
1940 i += 1;
1941 }
1942 Ok(())
1943 }
1944}
1945
1946impl fmt::Display for LayoutManifest {
1947 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1948 writeln!(
1949 f,
1950 "{} v{} (disc={}, size={})",
1951 self.name, self.version, self.disc, self.total_size
1952 )?;
1953 write!(f, " layout_id: ")?;
1954 write_hex(f, &self.layout_id)?;
1955 writeln!(f)?;
1956 let mut i = 0;
1957 while i < self.field_count {
1958 let field = &self.fields[i];
1959 writeln!(
1960 f,
1961 " [{:>3}..{:>3}] {} : {} ({} bytes)",
1962 field.offset,
1963 field.offset + field.size,
1964 field.name,
1965 field.canonical_type,
1966 field.size,
1967 )?;
1968 i += 1;
1969 }
1970 Ok(())
1971 }
1972}
1973
1974pub fn format_header(data: &[u8]) -> Option<DecodedHeader> {
1978 decode_header(data)
1979}
1980
1981pub fn format_segment_map<const N: usize>(data: &[u8]) -> Option<SegmentMap<N>> {
1985 let (count, segments) = decode_segments::<N>(data)?;
1986 Some(SegmentMap { count, segments })
1987}
1988
1989pub struct SegmentMap<const N: usize> {
1991 pub count: usize,
1992 pub segments: [DecodedSegment; N],
1993}
1994
1995impl<const N: usize> fmt::Display for SegmentMap<N> {
1996 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1997 writeln!(f, "Segment Map ({} segments):", self.count)?;
1998 let reg_end = HEADER_LEN + 4 + self.count * 16;
1999 writeln!(f, " [ 0..{:>3}] Header", HEADER_LEN)?;
2000 writeln!(f, " [{:>3}..{:>3}] Registry", HEADER_LEN, reg_end)?;
2001 let mut i = 0;
2002 while i < self.count {
2003 let seg = &self.segments[i];
2004 let end = seg.offset + seg.size;
2005 write!(f, " [{:>3}..{:>3}] Segment {} (id=", seg.offset, end, i)?;
2006 write_hex(f, &seg.id)?;
2007 writeln!(f, ", {} bytes, v{})", seg.size, seg.version)?;
2008 i += 1;
2009 }
2010 Ok(())
2011 }
2012}
2013
2014fn write_hex(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
2016 for b in bytes {
2017 write!(f, "{:02x}", b)?;
2018 }
2019 Ok(())
2020}
2021
2022#[derive(Clone, Copy, Debug)]
2028pub struct AccountEntry {
2029 pub name: &'static str,
2031 pub writable: bool,
2033 pub signer: bool,
2035 pub layout_ref: &'static str,
2037}
2038
2039#[derive(Clone, Copy, Debug)]
2041pub struct ArgDescriptor {
2042 pub name: &'static str,
2044 pub canonical_type: &'static str,
2046 pub size: u16,
2048}
2049
2050#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2056pub enum ArgParseError {
2057 TooShort {
2059 required: u16,
2061 got: u16,
2063 },
2064}
2065
2066impl fmt::Display for ArgParseError {
2067 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2068 match self {
2069 ArgParseError::TooShort { required, got } => {
2070 write!(f, "args: too short (required {}, got {})", required, got)
2071 }
2072 }
2073 }
2074}
2075
2076#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2089pub struct ErrorDescriptor {
2090 pub name: &'static str,
2092 pub code: u32,
2094 pub invariant: &'static str,
2096 pub doc: &'static str,
2098}
2099
2100#[derive(Clone, Copy, Debug)]
2112pub struct ConstantDescriptor {
2113 pub name: &'static str,
2115 pub ty: &'static str,
2117 pub value: &'static str,
2119 pub docs: &'static str,
2121}
2122
2123#[derive(Clone, Copy, Debug)]
2129pub struct ErrorRegistry {
2130 pub enum_name: &'static str,
2132 pub errors: &'static [ErrorDescriptor],
2134}
2135
2136impl ErrorRegistry {
2137 pub fn find_by_code(&self, code: u32) -> Option<&ErrorDescriptor> {
2139 let mut i = 0;
2140 while i < self.errors.len() {
2141 if self.errors[i].code == code {
2142 return Some(&self.errors[i]);
2143 }
2144 i += 1;
2145 }
2146 None
2147 }
2148
2149 pub fn invariant_for(&self, code: u32) -> Option<&'static str> {
2151 self.find_by_code(code).and_then(|d| {
2152 if d.invariant.is_empty() {
2153 None
2154 } else {
2155 Some(d.invariant)
2156 }
2157 })
2158 }
2159}
2160
2161#[derive(Clone, Copy, Debug)]
2163pub struct InstructionDescriptor {
2164 pub name: &'static str,
2166 pub tag: u8,
2168 pub args: &'static [ArgDescriptor],
2170 pub accounts: &'static [AccountEntry],
2172 pub capabilities: &'static [&'static str],
2174 pub policy_pack: &'static str,
2176 pub receipt_expected: bool,
2178}
2179
2180#[derive(Clone, Copy)]
2182pub struct EventDescriptor {
2183 pub name: &'static str,
2185 pub tag: u8,
2187 pub fields: &'static [FieldDescriptor],
2189}
2190
2191#[derive(Clone, Copy)]
2193pub struct PolicyDescriptor {
2194 pub name: &'static str,
2196 pub capabilities: &'static [&'static str],
2198 pub requirements: &'static [&'static str],
2200 pub invariants: &'static [&'static str],
2202 pub receipt_profile: &'static str,
2204}
2205
2206#[derive(Clone, Copy)]
2211pub struct LayoutMetadata {
2212 pub name: &'static str,
2214 pub segment_roles: &'static [&'static str],
2216 pub append_safe: bool,
2218 pub migration_required: bool,
2220 pub rebuildable: bool,
2222 pub policy_pack: &'static str,
2224 pub invariant_pack: &'static [&'static str],
2226 pub receipt_profile: &'static str,
2228 pub phase_requirements: &'static [&'static str],
2230 pub trust_profile: &'static str,
2232 pub manager_hints: &'static [&'static str],
2234}
2235
2236#[derive(Clone, Copy)]
2238pub struct CompatibilityPair {
2239 pub from_layout: &'static str,
2241 pub from_version: u8,
2243 pub to_layout: &'static str,
2245 pub to_version: u8,
2247 pub policy: MigrationPolicy,
2249 pub backward_readable: bool,
2251}
2252
2253#[derive(Clone, Copy)]
2268pub struct ProgramManifest {
2269 pub name: &'static str,
2271 pub version: &'static str,
2273 pub description: &'static str,
2275 pub layouts: &'static [LayoutManifest],
2277 pub layout_metadata: &'static [LayoutMetadata],
2279 pub instructions: &'static [InstructionDescriptor],
2281 pub events: &'static [EventDescriptor],
2283 pub policies: &'static [PolicyDescriptor],
2285 pub compatibility_pairs: &'static [CompatibilityPair],
2287 pub tooling_hints: &'static [&'static str],
2289 pub contexts: &'static [crate::accounts::ContextDescriptor],
2291}
2292
2293#[derive(Clone, Copy)]
2299pub struct PdaSeedHint {
2300 pub kind: &'static str,
2302 pub value: &'static str,
2304}
2305
2306#[derive(Clone, Copy)]
2308pub struct IdlAccountEntry {
2309 pub name: &'static str,
2311 pub writable: bool,
2313 pub signer: bool,
2315 pub layout_ref: &'static str,
2317 pub pda_seeds: &'static [PdaSeedHint],
2319}
2320
2321#[derive(Clone, Copy)]
2323pub struct IdlInstructionDescriptor {
2324 pub name: &'static str,
2326 pub tag: u8,
2328 pub args: &'static [ArgDescriptor],
2330 pub accounts: &'static [IdlAccountEntry],
2332}
2333
2334#[derive(Clone, Copy)]
2342pub struct ProgramIdl {
2343 pub name: &'static str,
2345 pub version: &'static str,
2347 pub description: &'static str,
2349 pub instructions: &'static [IdlInstructionDescriptor],
2351 pub accounts: &'static [LayoutManifest],
2353 pub events: &'static [EventDescriptor],
2355 pub fingerprints: &'static [([u8; 8], &'static str)],
2357}
2358
2359impl ProgramIdl {
2360 pub const fn empty() -> Self {
2362 Self {
2363 name: "",
2364 version: "",
2365 description: "",
2366 instructions: &[],
2367 accounts: &[],
2368 events: &[],
2369 fingerprints: &[],
2370 }
2371 }
2372
2373 pub const fn instruction_count(&self) -> usize {
2375 self.instructions.len()
2376 }
2377
2378 pub const fn account_count(&self) -> usize {
2380 self.accounts.len()
2381 }
2382
2383 pub fn find_instruction(&self, name: &str) -> Option<&IdlInstructionDescriptor> {
2385 let mut i = 0;
2386 while i < self.instructions.len() {
2387 if const_str_eq(self.instructions[i].name, name) {
2388 return Some(&self.instructions[i]);
2389 }
2390 i += 1;
2391 }
2392 None
2393 }
2394
2395 pub fn find_account(&self, name: &str) -> Option<&LayoutManifest> {
2397 let mut i = 0;
2398 while i < self.accounts.len() {
2399 if const_str_eq(self.accounts[i].name, name) {
2400 return Some(&self.accounts[i]);
2401 }
2402 i += 1;
2403 }
2404 None
2405 }
2406}
2407
2408impl fmt::Display for ProgramIdl {
2409 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2410 writeln!(f, "IDL: {} {}", self.name, self.version)?;
2411 if !self.description.is_empty() {
2412 writeln!(f, " {}", self.description)?;
2413 }
2414 writeln!(f)?;
2415 writeln!(f, "Instructions ({}):", self.instructions.len())?;
2416 for ix in self.instructions.iter() {
2417 write!(
2418 f,
2419 " {:>2} {:16} args={} accounts={}",
2420 ix.tag,
2421 ix.name,
2422 ix.args.len(),
2423 ix.accounts.len()
2424 )?;
2425 writeln!(f)?;
2426 }
2427 writeln!(f)?;
2428 writeln!(f, "Accounts ({}):", self.accounts.len())?;
2429 for a in self.accounts.iter() {
2430 write!(
2431 f,
2432 " {:16} disc={} v{} {} bytes id=",
2433 a.name, a.disc, a.version, a.total_size
2434 )?;
2435 write_hex(f, &a.layout_id)?;
2436 writeln!(f)?;
2437 }
2438 if !self.events.is_empty() {
2439 writeln!(f)?;
2440 writeln!(f, "Events ({}):", self.events.len())?;
2441 for e in self.events.iter() {
2442 writeln!(f, " {:>2} {:16} fields={}", e.tag, e.name, e.fields.len())?;
2443 }
2444 }
2445 Ok(())
2446 }
2447}
2448
2449#[derive(Clone, Copy)]
2457pub struct CodamaInstruction {
2458 pub name: &'static str,
2459 pub discriminator: u8,
2460 pub args: &'static [ArgDescriptor],
2461 pub accounts: &'static [IdlAccountEntry],
2462}
2463
2464#[derive(Clone, Copy)]
2466pub struct CodamaAccount {
2467 pub name: &'static str,
2468 pub discriminator: u8,
2469 pub size: usize,
2470 pub fields: &'static [FieldDescriptor],
2471}
2472
2473#[derive(Clone, Copy)]
2475pub struct CodamaEvent {
2476 pub name: &'static str,
2477 pub discriminator: u8,
2478 pub fields: &'static [FieldDescriptor],
2479}
2480
2481#[derive(Clone, Copy)]
2497pub struct CodamaProjection {
2498 pub name: &'static str,
2500 pub version: &'static str,
2502 pub instructions: &'static [CodamaInstruction],
2504 pub accounts: &'static [CodamaAccount],
2506 pub events: &'static [CodamaEvent],
2508}
2509
2510impl CodamaProjection {
2511 pub const fn empty() -> Self {
2513 Self {
2514 name: "",
2515 version: "",
2516 instructions: &[],
2517 accounts: &[],
2518 events: &[],
2519 }
2520 }
2521}
2522
2523impl fmt::Display for CodamaProjection {
2524 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2525 writeln!(f, "Codama: {} {}", self.name, self.version)?;
2526 writeln!(f)?;
2527 writeln!(f, "Instructions ({}):", self.instructions.len())?;
2528 for ix in self.instructions.iter() {
2529 writeln!(
2530 f,
2531 " {:>2} {:16} args={} accounts={}",
2532 ix.discriminator,
2533 ix.name,
2534 ix.args.len(),
2535 ix.accounts.len()
2536 )?;
2537 }
2538 writeln!(f)?;
2539 writeln!(f, "Accounts ({}):", self.accounts.len())?;
2540 for a in self.accounts.iter() {
2541 writeln!(
2542 f,
2543 " {:16} disc={} {} bytes fields={}",
2544 a.name,
2545 a.discriminator,
2546 a.size,
2547 a.fields.len()
2548 )?;
2549 }
2550 if !self.events.is_empty() {
2551 writeln!(f)?;
2552 writeln!(f, "Events ({}):", self.events.len())?;
2553 for e in self.events.iter() {
2554 writeln!(
2555 f,
2556 " {:>2} {:16} fields={}",
2557 e.discriminator,
2558 e.name,
2559 e.fields.len()
2560 )?;
2561 }
2562 }
2563 Ok(())
2564 }
2565}
2566
2567impl ProgramManifest {
2568 pub const fn empty() -> Self {
2570 Self {
2571 name: "",
2572 version: "",
2573 description: "",
2574 layouts: &[],
2575 layout_metadata: &[],
2576 instructions: &[],
2577 events: &[],
2578 policies: &[],
2579 compatibility_pairs: &[],
2580 tooling_hints: &[],
2581 contexts: &[],
2582 }
2583 }
2584
2585 pub const fn layout_count(&self) -> usize {
2587 self.layouts.len()
2588 }
2589
2590 pub const fn instruction_count(&self) -> usize {
2592 self.instructions.len()
2593 }
2594
2595 pub fn find_layout_by_disc(&self, disc: u8) -> Option<&LayoutManifest> {
2597 let mut i = 0;
2598 while i < self.layouts.len() {
2599 if self.layouts[i].disc == disc {
2600 return Some(&self.layouts[i]);
2601 }
2602 i += 1;
2603 }
2604 None
2605 }
2606
2607 pub fn find_layout_by_id(&self, layout_id: &[u8; 8]) -> Option<&LayoutManifest> {
2609 let mut i = 0;
2610 while i < self.layouts.len() {
2611 if self.layouts[i].layout_id == *layout_id {
2612 return Some(&self.layouts[i]);
2613 }
2614 i += 1;
2615 }
2616 None
2617 }
2618
2619 pub fn identify_from_data(&self, data: &[u8]) -> Option<&LayoutManifest> {
2621 let header = decode_header(data)?;
2622 if let Some(m) = self.find_layout_by_id(&header.layout_id) {
2624 return Some(m);
2625 }
2626 self.find_layout_by_disc(header.disc)
2628 }
2629
2630 pub fn find_instruction(&self, tag: u8) -> Option<&InstructionDescriptor> {
2632 let mut i = 0;
2633 while i < self.instructions.len() {
2634 if self.instructions[i].tag == tag {
2635 return Some(&self.instructions[i]);
2636 }
2637 i += 1;
2638 }
2639 None
2640 }
2641
2642 pub fn find_policy(&self, name: &str) -> Option<&PolicyDescriptor> {
2644 let mut i = 0;
2645 while i < self.policies.len() {
2646 if self.policies[i].name == name {
2647 return Some(&self.policies[i]);
2648 }
2649 i += 1;
2650 }
2651 None
2652 }
2653
2654 pub fn find_layout_metadata(&self, name: &str) -> Option<&LayoutMetadata> {
2656 let mut i = 0;
2657 while i < self.layout_metadata.len() {
2658 if const_str_eq(self.layout_metadata[i].name, name) {
2659 return Some(&self.layout_metadata[i]);
2660 }
2661 i += 1;
2662 }
2663 None
2664 }
2665
2666 pub fn find_compat_pair(
2668 &self,
2669 from_name: &str,
2670 from_ver: u8,
2671 to_name: &str,
2672 to_ver: u8,
2673 ) -> Option<&CompatibilityPair> {
2674 let mut i = 0;
2675 while i < self.compatibility_pairs.len() {
2676 let cp = &self.compatibility_pairs[i];
2677 if const_str_eq(cp.from_layout, from_name)
2678 && cp.from_version == from_ver
2679 && const_str_eq(cp.to_layout, to_name)
2680 && cp.to_version == to_ver
2681 {
2682 return Some(cp);
2683 }
2684 i += 1;
2685 }
2686 None
2687 }
2688}
2689
2690impl fmt::Display for ProgramManifest {
2691 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2692 writeln!(f, "Program: {} {}", self.name, self.version)?;
2693 if !self.description.is_empty() {
2694 writeln!(f, " {}", self.description)?;
2695 }
2696 writeln!(f)?;
2697
2698 writeln!(f, "Layouts ({}):", self.layouts.len())?;
2699 for m in self.layouts.iter() {
2700 write!(
2701 f,
2702 " {:16} v{} disc={} {} bytes fingerprint=",
2703 m.name, m.version, m.disc, m.total_size
2704 )?;
2705 write_hex(f, &m.layout_id)?;
2706 if let Some(meta) = self.find_layout_metadata(m.name) {
2708 if !meta.trust_profile.is_empty() {
2709 write!(f, " trust={}", meta.trust_profile)?;
2710 }
2711 if meta.append_safe {
2712 write!(f, " append-safe")?;
2713 }
2714 if meta.migration_required {
2715 write!(f, " migration-required")?;
2716 }
2717 }
2718 writeln!(f)?;
2719 }
2720 writeln!(f)?;
2721
2722 writeln!(f, "Instructions ({}):", self.instructions.len())?;
2723 for ix in self.instructions.iter() {
2724 write!(
2725 f,
2726 " {:>2} {:16} accounts={}",
2727 ix.tag,
2728 ix.name,
2729 ix.accounts.len()
2730 )?;
2731 if !ix.capabilities.is_empty() {
2732 write!(f, " caps=")?;
2733 for (j, c) in ix.capabilities.iter().enumerate() {
2734 if j > 0 {
2735 write!(f, ",")?;
2736 }
2737 write!(f, "{}", c)?;
2738 }
2739 }
2740 if ix.receipt_expected {
2741 write!(f, " receipt=yes")?;
2742 }
2743 writeln!(f)?;
2744 }
2745 writeln!(f)?;
2746
2747 if !self.policies.is_empty() {
2748 writeln!(f, "Policies ({}):", self.policies.len())?;
2749 for p in self.policies.iter() {
2750 write!(f, " {:24}", p.name)?;
2751 for (j, r) in p.requirements.iter().enumerate() {
2752 if j > 0 {
2753 write!(f, " + ")?;
2754 }
2755 write!(f, "{}", r)?;
2756 }
2757 if !p.receipt_profile.is_empty() {
2758 write!(f, " receipt={}", p.receipt_profile)?;
2759 }
2760 writeln!(f)?;
2761 }
2762 writeln!(f)?;
2763 }
2764
2765 if !self.events.is_empty() {
2766 writeln!(f, "Events ({}):", self.events.len())?;
2767 for e in self.events.iter() {
2768 writeln!(f, " {:>2} {:16} fields={}", e.tag, e.name, e.fields.len())?;
2769 }
2770 writeln!(f)?;
2771 }
2772
2773 if !self.compatibility_pairs.is_empty() {
2774 writeln!(f, "Compatibility ({}):", self.compatibility_pairs.len())?;
2775 for cp in self.compatibility_pairs.iter() {
2776 writeln!(
2777 f,
2778 " {} v{} -> {} v{} {}{}",
2779 cp.from_layout,
2780 cp.from_version,
2781 cp.to_layout,
2782 cp.to_version,
2783 cp.policy,
2784 if cp.backward_readable {
2785 " backward-readable"
2786 } else {
2787 ""
2788 },
2789 )?;
2790 }
2791 }
2792
2793 Ok(())
2794 }
2795}
2796
2797pub struct DecodedField<'a> {
2803 pub name: &'a str,
2805 pub canonical_type: &'a str,
2807 pub raw: &'a [u8],
2809 pub offset: u16,
2811 pub size: u16,
2813}
2814
2815impl<'a> DecodedField<'a> {
2816 pub fn format_value(&self, buf: &mut [u8]) -> usize {
2820 match self.canonical_type {
2821 "WireU64" | "LeU64" if self.raw.len() >= 8 => {
2822 let v = u64::from_le_bytes([
2823 self.raw[0],
2824 self.raw[1],
2825 self.raw[2],
2826 self.raw[3],
2827 self.raw[4],
2828 self.raw[5],
2829 self.raw[6],
2830 self.raw[7],
2831 ]);
2832 format_u64(v, buf)
2833 }
2834 "WireU32" | "LeU32" if self.raw.len() >= 4 => {
2835 let v =
2836 u32::from_le_bytes([self.raw[0], self.raw[1], self.raw[2], self.raw[3]]) as u64;
2837 format_u64(v, buf)
2838 }
2839 "WireU16" | "LeU16" if self.raw.len() >= 2 => {
2840 let v = u16::from_le_bytes([self.raw[0], self.raw[1]]) as u64;
2841 format_u64(v, buf)
2842 }
2843 "WireBool" | "LeBool" if !self.raw.is_empty() => {
2844 if self.raw[0] != 0 {
2845 let len = 4usize.min(buf.len());
2846 buf[..len].copy_from_slice(&b"true"[..len]);
2847 len
2848 } else {
2849 let len = 5usize.min(buf.len());
2850 buf[..len].copy_from_slice(&b"false"[..len]);
2851 len
2852 }
2853 }
2854 "u8" if self.raw.len() == 1 => format_u64(self.raw[0] as u64, buf),
2855 _ if self.size == 32 => {
2856 format_hex_truncated(self.raw, buf)
2858 }
2859 _ => format_hex_truncated(self.raw, buf),
2860 }
2861 }
2862}
2863
2864pub fn decode_account_fields<'a, const N: usize>(
2868 data: &'a [u8],
2869 manifest: &'a LayoutManifest,
2870) -> (usize, [Option<DecodedField<'a>>; N]) {
2871 let mut fields: [Option<DecodedField<'a>>; N] = [const { None }; N];
2872 let count = manifest.field_count.min(N);
2873 let mut i = 0;
2874 while i < count {
2875 let fd = &manifest.fields[i];
2876 let start = fd.offset as usize;
2877 let end = start + fd.size as usize;
2878 if end <= data.len() {
2879 fields[i] = Some(DecodedField {
2880 name: fd.name,
2881 canonical_type: fd.canonical_type,
2882 raw: &data[start..end],
2883 offset: fd.offset,
2884 size: fd.size,
2885 });
2886 }
2887 i += 1;
2888 }
2889 (count, fields)
2890}
2891
2892fn format_u64(mut v: u64, buf: &mut [u8]) -> usize {
2894 if v == 0 {
2895 if !buf.is_empty() {
2896 buf[0] = b'0';
2897 return 1;
2898 }
2899 return 0;
2900 }
2901 let mut tmp = [0u8; 20];
2903 let mut pos = 0;
2904 while v > 0 && pos < 20 {
2905 tmp[pos] = b'0' + (v % 10) as u8;
2906 v /= 10;
2907 pos += 1;
2908 }
2909 let len = pos.min(buf.len());
2910 let mut i = 0;
2911 while i < len {
2912 buf[i] = tmp[pos - 1 - i];
2913 i += 1;
2914 }
2915 len
2916}
2917
2918fn format_hex_truncated(bytes: &[u8], buf: &mut [u8]) -> usize {
2920 const HEX: &[u8; 16] = b"0123456789abcdef";
2921 let max_bytes = if bytes.len() > 8 { 8 } else { bytes.len() };
2922 let mut pos = 0;
2923 if buf.len() >= 2 {
2925 buf[0] = b'0';
2926 buf[1] = b'x';
2927 pos = 2;
2928 }
2929 let mut i = 0;
2930 while i < max_bytes && pos + 1 < buf.len() {
2931 buf[pos] = HEX[(bytes[i] >> 4) as usize];
2932 buf[pos + 1] = HEX[(bytes[i] & 0xf) as usize];
2933 pos += 2;
2934 i += 1;
2935 }
2936 if bytes.len() > 8 && pos + 3 <= buf.len() {
2937 buf[pos] = b'.';
2938 buf[pos + 1] = b'.';
2939 buf[pos + 2] = b'.';
2940 pos += 3;
2941 }
2942 pos
2943}
2944
2945#[repr(C)]
2968#[derive(Clone, Copy)]
2969pub struct HopperSchemaPointer {
2970 pub schema_version: u16,
2972 pub pointer_flags: u16,
2974 pub manifest_hash: [u8; 32],
2976 pub idl_hash: [u8; 32],
2978 pub codama_hash: [u8; 32],
2980 pub uri_len: u16,
2982 pub uri: [u8; 192],
2984}
2985
2986impl HopperSchemaPointer {
2987 pub const DISC: u8 = 255;
2989
2990 pub const PAYLOAD_LEN: usize = 2 + 2 + 32 + 32 + 32 + 2 + 192; pub const ACCOUNT_LEN: usize = HEADER_LEN + Self::PAYLOAD_LEN; pub const PDA_SEED: &'static [u8] = b"hopper-schema";
2998
2999 pub const FLAG_HAS_MANIFEST: u16 = 0x0001;
3001 pub const FLAG_HAS_IDL: u16 = 0x0002;
3002 pub const FLAG_HAS_CODAMA: u16 = 0x0004;
3003 pub const FLAG_HAS_URI: u16 = 0x0008;
3004 pub const FLAG_URI_IS_IPFS: u16 = 0x0010;
3005 pub const FLAG_URI_IS_ARWEAVE: u16 = 0x0020;
3006
3007 pub fn uri_str(&self) -> &str {
3009 let len = (self.uri_len as usize).min(192);
3010 core::str::from_utf8(&self.uri[..len]).unwrap_or("")
3012 }
3013
3014 #[inline(always)]
3016 pub fn has_flag(&self, flag: u16) -> bool {
3017 self.pointer_flags & flag != 0
3018 }
3019}
3020
3021#[derive(Clone, Copy, Debug)]
3028pub struct SemanticLint {
3029 pub severity: LintSeverity,
3031 pub code: &'static str,
3033 pub message: &'static str,
3035 pub field: &'static str,
3037}
3038
3039#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3041#[repr(u8)]
3042pub enum LintSeverity {
3043 Info = 0,
3045 Warning = 1,
3047 Error = 2,
3049}
3050
3051impl LintSeverity {
3052 pub const fn name(self) -> &'static str {
3054 match self {
3055 Self::Info => "info",
3056 Self::Warning => "warning",
3057 Self::Error => "error",
3058 }
3059 }
3060}
3061
3062pub fn lint_layout<const N: usize>(
3066 manifest: &LayoutManifest,
3067 behavior: &LayoutBehavior,
3068) -> (usize, [SemanticLint; N]) {
3069 let mut lints = [SemanticLint {
3070 severity: LintSeverity::Info,
3071 code: "",
3072 message: "",
3073 field: "",
3074 }; N];
3075 let mut count = 0usize;
3076
3077 let mut i = 0;
3078 while i < manifest.field_count {
3079 let field = &manifest.fields[i];
3080
3081 if field.intent.is_authority_sensitive()
3083 && behavior.mutation_class.is_mutating()
3084 && !behavior.requires_signer
3085 {
3086 if count < N {
3087 lints[count] = SemanticLint {
3088 severity: LintSeverity::Error,
3089 code: "E001",
3090 message:
3091 "Authority-sensitive field in mutable layout without signer requirement",
3092 field: field.name,
3093 };
3094 count += 1;
3095 }
3096 }
3097
3098 if field.intent.is_monetary()
3100 && behavior.mutation_class.is_mutating()
3101 && !matches!(behavior.mutation_class, MutationClass::Financial)
3102 {
3103 if count < N {
3104 lints[count] = SemanticLint {
3105 severity: LintSeverity::Warning,
3106 code: "W001",
3107 message: "Monetary field in layout without financial mutation class",
3108 field: field.name,
3109 };
3110 count += 1;
3111 }
3112 }
3113
3114 if field.intent.is_init_only()
3116 && behavior.mutation_class.is_mutating()
3117 && !matches!(behavior.mutation_class, MutationClass::AppendOnly)
3118 {
3119 if count < N {
3120 lints[count] = SemanticLint {
3121 severity: LintSeverity::Warning,
3122 code: "W002",
3123 message: "Init-only field (PDA seed or bump) in mutable layout. Consider making read-only or append-only.",
3124 field: field.name,
3125 };
3126 count += 1;
3127 }
3128 }
3129
3130 i += 1;
3131 }
3132
3133 if behavior.mutation_class.is_mutating() && !behavior.requires_signer {
3137 if count < N {
3138 lints[count] = SemanticLint {
3139 severity: LintSeverity::Warning,
3140 code: "W003",
3141 message: "Mutable layout does not require signer. Verify this is intentional.",
3142 field: "",
3143 };
3144 count += 1;
3145 }
3146 }
3147
3148 if behavior.affects_balance {
3150 let mut has_balance = false;
3151 let mut j = 0;
3152 while j < manifest.field_count {
3153 if manifest.fields[j].intent.is_monetary() {
3154 has_balance = true;
3155 }
3156 j += 1;
3157 }
3158 if !has_balance && count < N {
3159 lints[count] = SemanticLint {
3160 severity: LintSeverity::Warning,
3161 code: "W004",
3162 message: "Layout behavior declares affects_balance but no monetary fields found",
3163 field: "",
3164 };
3165 count += 1;
3166 }
3167 }
3168
3169 (count, lints)
3170}
3171
3172#[cfg(feature = "policy")]
3178pub fn lint_policy<const N: usize>(
3179 behavior: &LayoutBehavior,
3180 policy: hopper_core::policy::PolicyClass,
3181) -> (usize, [SemanticLint; N]) {
3182 let mut lints = [SemanticLint {
3183 severity: LintSeverity::Info,
3184 code: "",
3185 message: "",
3186 field: "",
3187 }; N];
3188 let mut count = 0usize;
3189
3190 if matches!(behavior.mutation_class, MutationClass::Financial)
3192 && !matches!(policy, hopper_core::policy::PolicyClass::Financial)
3193 {
3194 if count < N {
3195 lints[count] = SemanticLint {
3196 severity: LintSeverity::Warning,
3197 code: "W005",
3198 message: "Financial mutation class but policy class is not Financial",
3199 field: "",
3200 };
3201 count += 1;
3202 }
3203 }
3204
3205 if matches!(policy, hopper_core::policy::PolicyClass::Financial)
3207 && !matches!(behavior.mutation_class, MutationClass::Financial)
3208 {
3209 if count < N {
3210 lints[count] = SemanticLint {
3211 severity: LintSeverity::Warning,
3212 code: "W006",
3213 message: "Financial policy class but mutation class is not Financial",
3214 field: "",
3215 };
3216 count += 1;
3217 }
3218 }
3219
3220 (count, lints)
3221}
3222
3223impl fmt::Display for SemanticLint {
3224 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3225 write!(
3226 f,
3227 "[{}] {}: {}",
3228 self.severity.name(),
3229 self.code,
3230 self.message
3231 )?;
3232 if !self.field.is_empty() {
3233 write!(f, " (field: {})", self.field)?;
3234 }
3235 Ok(())
3236 }
3237}
3238
3239pub struct OperatingProfile {
3248 pub financial_fields: [&'static str; 16],
3250 pub financial_count: u8,
3252 pub authority_surfaces: [&'static str; 16],
3254 pub authority_count: u8,
3256 pub append_only_segments: [&'static str; 8],
3258 pub append_only_count: u8,
3260 pub migration_sensitive: [&'static str; 8],
3262 pub migration_sensitive_count: u8,
3264 pub stability_grades: [(&'static str, LayoutStabilityGrade); 8],
3266 pub stability_count: u8,
3268 pub has_financial_ops: bool,
3270 pub has_cpi_ops: bool,
3272 pub has_migration_paths: bool,
3274 pub has_receipts: bool,
3276}
3277
3278impl OperatingProfile {
3279 pub fn from_manifest(manifest: &ProgramManifest) -> Self {
3281 let mut profile = Self {
3282 financial_fields: [""; 16],
3283 financial_count: 0,
3284 authority_surfaces: [""; 16],
3285 authority_count: 0,
3286 append_only_segments: [""; 8],
3287 append_only_count: 0,
3288 migration_sensitive: [""; 8],
3289 migration_sensitive_count: 0,
3290 stability_grades: [("", LayoutStabilityGrade::Stable); 8],
3291 stability_count: 0,
3292 has_financial_ops: false,
3293 has_cpi_ops: false,
3294 has_migration_paths: !manifest.compatibility_pairs.is_empty(),
3295 has_receipts: false,
3296 };
3297
3298 let mut li = 0;
3300 while li < manifest.layouts.len() {
3301 let layout = &manifest.layouts[li];
3302
3303 if (profile.stability_count as usize) < 8 {
3305 profile.stability_grades[profile.stability_count as usize] =
3306 (layout.name, LayoutStabilityGrade::compute(layout));
3307 profile.stability_count += 1;
3308 }
3309
3310 let mut fi = 0;
3311 while fi < layout.field_count {
3312 let field = &layout.fields[fi];
3313 if field.intent.is_monetary() && (profile.financial_count as usize) < 16 {
3314 profile.financial_fields[profile.financial_count as usize] = field.name;
3315 profile.financial_count += 1;
3316 }
3317 if field.intent.is_authority_sensitive() && (profile.authority_count as usize) < 16
3318 {
3319 profile.authority_surfaces[profile.authority_count as usize] = field.name;
3320 profile.authority_count += 1;
3321 }
3322 fi += 1;
3323 }
3324 li += 1;
3325 }
3326
3327 let mut mi = 0;
3329 while mi < manifest.layout_metadata.len() {
3330 let meta = &manifest.layout_metadata[mi];
3331 let mut si = 0;
3332 while si < meta.segment_roles.len() {
3333 let role_name = meta.segment_roles[si];
3334 if (const_str_eq(role_name, "Journal") || const_str_eq(role_name, "Audit"))
3335 && (profile.append_only_count as usize) < 8
3336 {
3337 profile.append_only_segments[profile.append_only_count as usize] = role_name;
3338 profile.append_only_count += 1;
3339 }
3340 if const_str_eq(role_name, "Core")
3341 && (profile.migration_sensitive_count as usize) < 8
3342 {
3343 profile.migration_sensitive[profile.migration_sensitive_count as usize] =
3344 meta.name;
3345 profile.migration_sensitive_count += 1;
3346 }
3347 si += 1;
3348 }
3349 mi += 1;
3350 }
3351
3352 let mut ii = 0;
3354 while ii < manifest.instructions.len() {
3355 let ix = &manifest.instructions[ii];
3356 if ix.receipt_expected {
3357 profile.has_receipts = true;
3358 }
3359 let mut ci = 0;
3360 while ci < ix.capabilities.len() {
3361 if const_str_eq(ix.capabilities[ci], "MutatesTreasury") {
3362 profile.has_financial_ops = true;
3363 }
3364 if const_str_eq(ix.capabilities[ci], "ExternalCall") {
3365 profile.has_cpi_ops = true;
3366 }
3367 ci += 1;
3368 }
3369 ii += 1;
3370 }
3371
3372 profile
3373 }
3374}
3375
3376impl fmt::Display for OperatingProfile {
3377 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3378 writeln!(f, "Operating Profile:")?;
3379
3380 if self.financial_count > 0 {
3381 write!(f, " Financial fields:")?;
3382 let mut i = 0;
3383 while i < self.financial_count as usize {
3384 write!(f, " {}", self.financial_fields[i])?;
3385 i += 1;
3386 }
3387 writeln!(f)?;
3388 }
3389
3390 if self.authority_count > 0 {
3391 write!(f, " Authority surfaces:")?;
3392 let mut i = 0;
3393 while i < self.authority_count as usize {
3394 write!(f, " {}", self.authority_surfaces[i])?;
3395 i += 1;
3396 }
3397 writeln!(f)?;
3398 }
3399
3400 if self.append_only_count > 0 {
3401 write!(f, " Append-only segments:")?;
3402 let mut i = 0;
3403 while i < self.append_only_count as usize {
3404 write!(f, " {}", self.append_only_segments[i])?;
3405 i += 1;
3406 }
3407 writeln!(f)?;
3408 }
3409
3410 if self.stability_count > 0 {
3411 writeln!(f, " Stability grades:")?;
3412 let mut i = 0;
3413 while i < self.stability_count as usize {
3414 let (name, grade) = self.stability_grades[i];
3415 writeln!(f, " {}: {}", name, grade.name())?;
3416 i += 1;
3417 }
3418 }
3419
3420 write!(f, " Features:")?;
3421 if self.has_financial_ops {
3422 write!(f, " financial")?;
3423 }
3424 if self.has_cpi_ops {
3425 write!(f, " cpi")?;
3426 }
3427 if self.has_migration_paths {
3428 write!(f, " migration")?;
3429 }
3430 if self.has_receipts {
3431 write!(f, " receipts")?;
3432 }
3433 writeln!(f)?;
3434
3435 Ok(())
3436 }
3437}
3438
3439pub struct HopperIdl {
3449 pub base: ProgramIdl,
3451 pub policies: &'static [PolicyDescriptor],
3453 pub compatibility: &'static [CompatibilityPair],
3455 pub receipt_profiles: &'static [ReceiptProfile],
3457 pub segment_metadata: &'static [IdlSegmentDescriptor],
3459 pub contexts: &'static [crate::accounts::ContextDescriptor],
3461}
3462
3463#[derive(Clone, Copy)]
3465pub struct ReceiptProfile {
3466 pub name: &'static str,
3468 pub expected_phase: &'static str,
3470 pub expects_balance_change: bool,
3472 pub expects_authority_change: bool,
3474 pub expects_journal_append: bool,
3476 pub min_changed_fields: u8,
3478}
3479
3480impl fmt::Display for ReceiptProfile {
3481 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3482 write!(f, "{}(phase={}", self.name, self.expected_phase)?;
3483 if self.expects_balance_change {
3484 write!(f, " balance")?;
3485 }
3486 if self.expects_authority_change {
3487 write!(f, " authority")?;
3488 }
3489 if self.expects_journal_append {
3490 write!(f, " journal")?;
3491 }
3492 if self.min_changed_fields > 0 {
3493 write!(f, " min_fields={}", self.min_changed_fields)?;
3494 }
3495 write!(f, ")")
3496 }
3497}
3498
3499#[derive(Clone, Copy)]
3501pub struct IdlSegmentDescriptor {
3502 pub name: &'static str,
3504 pub role: &'static str,
3506 pub append_only: bool,
3508 pub rebuildable: bool,
3510 pub must_preserve: bool,
3512}
3513
3514impl fmt::Display for IdlSegmentDescriptor {
3515 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3516 write!(f, "{}(role={}", self.name, self.role)?;
3517 if self.append_only {
3518 write!(f, " append-only")?;
3519 }
3520 if self.rebuildable {
3521 write!(f, " rebuildable")?;
3522 }
3523 if self.must_preserve {
3524 write!(f, " must-preserve")?;
3525 }
3526 write!(f, ")")
3527 }
3528}
3529
3530impl HopperIdl {
3531 pub const fn empty() -> Self {
3533 Self {
3534 base: ProgramIdl::empty(),
3535 policies: &[],
3536 compatibility: &[],
3537 receipt_profiles: &[],
3538 segment_metadata: &[],
3539 contexts: &[],
3540 }
3541 }
3542}
3543
3544impl fmt::Display for HopperIdl {
3545 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3546 write!(f, "{}", self.base)?;
3547
3548 if !self.policies.is_empty() {
3549 writeln!(f)?;
3550 writeln!(f, "Policies ({}):", self.policies.len())?;
3551 for p in self.policies.iter() {
3552 write!(f, " {:24}", p.name)?;
3553 for (j, r) in p.requirements.iter().enumerate() {
3554 if j > 0 {
3555 write!(f, " + ")?;
3556 }
3557 write!(f, "{}", r)?;
3558 }
3559 writeln!(f)?;
3560 }
3561 }
3562
3563 if !self.compatibility.is_empty() {
3564 writeln!(f)?;
3565 writeln!(f, "Compatibility ({}):", self.compatibility.len())?;
3566 for cp in self.compatibility.iter() {
3567 writeln!(
3568 f,
3569 " {} v{} -> {} v{} {}",
3570 cp.from_layout, cp.from_version, cp.to_layout, cp.to_version, cp.policy,
3571 )?;
3572 }
3573 }
3574
3575 if !self.receipt_profiles.is_empty() {
3576 writeln!(f)?;
3577 writeln!(f, "Receipt Profiles ({}):", self.receipt_profiles.len())?;
3578 for rp in self.receipt_profiles.iter() {
3579 writeln!(
3580 f,
3581 " {:24} phase={} balance={} authority={} journal={}",
3582 rp.name,
3583 rp.expected_phase,
3584 rp.expects_balance_change,
3585 rp.expects_authority_change,
3586 rp.expects_journal_append,
3587 )?;
3588 }
3589 }
3590
3591 if !self.segment_metadata.is_empty() {
3592 writeln!(f)?;
3593 writeln!(f, "Segments ({}):", self.segment_metadata.len())?;
3594 for s in self.segment_metadata.iter() {
3595 write!(f, " {:16} role={}", s.name, s.role)?;
3596 if s.append_only {
3597 write!(f, " append-only")?;
3598 }
3599 if s.rebuildable {
3600 write!(f, " rebuildable")?;
3601 }
3602 if s.must_preserve {
3603 write!(f, " must-preserve")?;
3604 }
3605 writeln!(f)?;
3606 }
3607 }
3608
3609 if !self.contexts.is_empty() {
3610 writeln!(f)?;
3611 writeln!(f, "Contexts ({}):", self.contexts.len())?;
3612 for ctx in self.contexts.iter() {
3613 write!(f, " {}", ctx)?;
3614 }
3615 }
3616
3617 Ok(())
3618 }
3619}
3620
3621#[derive(Clone, Copy, Debug)]
3627pub struct ManagerMetadata {
3628 pub layout: LayoutInfo,
3630 pub fields: &'static [FieldInfo],
3632}
3633
3634#[derive(Clone, Copy, Debug)]
3636pub struct SchemaBundle {
3637 pub manager: ManagerMetadata,
3638 pub manifest: LayoutManifest,
3639}
3640
3641pub trait SchemaExport: LayoutContract {
3655 #[inline(always)]
3657 fn layout_info() -> LayoutInfo {
3658 <Self as LayoutContract>::layout_info_static()
3659 }
3660
3661 #[inline(always)]
3663 fn field_map() -> &'static [FieldInfo] {
3664 <Self as LayoutContract>::fields()
3665 }
3666
3667 #[inline(always)]
3669 fn manager_metadata() -> ManagerMetadata {
3670 ManagerMetadata {
3671 layout: Self::layout_info(),
3672 fields: Self::field_map(),
3673 }
3674 }
3675
3676 #[inline(always)]
3678 fn schema_bundle() -> SchemaBundle {
3679 SchemaBundle {
3680 manager: Self::manager_metadata(),
3681 manifest: Self::layout_manifest(),
3682 }
3683 }
3684
3685 fn layout_manifest() -> LayoutManifest;
3687}
3688
3689pub trait AccountSchemaExt {
3691 fn manager_metadata_for<T: SchemaExport>(&self) -> Option<ManagerMetadata>;
3693
3694 fn schema_bundle_for<T: SchemaExport>(&self) -> Option<SchemaBundle>;
3696}
3697
3698impl AccountSchemaExt for AccountView {
3699 #[inline]
3700 fn manager_metadata_for<T: SchemaExport>(&self) -> Option<ManagerMetadata> {
3701 let info = self.layout_info()?;
3702 if info.matches::<T>() {
3703 Some(T::manager_metadata())
3704 } else {
3705 None
3706 }
3707 }
3708
3709 #[inline]
3710 fn schema_bundle_for<T: SchemaExport>(&self) -> Option<SchemaBundle> {
3711 let info = self.layout_info()?;
3712 if info.matches::<T>() {
3713 Some(T::schema_bundle())
3714 } else {
3715 None
3716 }
3717 }
3718}
3719
3720#[cfg(test)]
3723mod tests {
3724 use super::*;
3725
3726 const V1_FIELDS: &[FieldDescriptor] = &[
3727 FieldDescriptor {
3728 name: "authority",
3729 canonical_type: "[u8;32]",
3730 size: 32,
3731 offset: 16,
3732 intent: FieldIntent::Custom,
3733 },
3734 FieldDescriptor {
3735 name: "balance",
3736 canonical_type: "WireU64",
3737 size: 8,
3738 offset: 48,
3739 intent: FieldIntent::Custom,
3740 },
3741 ];
3742
3743 const V2_FIELDS: &[FieldDescriptor] = &[
3744 FieldDescriptor {
3745 name: "authority",
3746 canonical_type: "[u8;32]",
3747 size: 32,
3748 offset: 16,
3749 intent: FieldIntent::Custom,
3750 },
3751 FieldDescriptor {
3752 name: "balance",
3753 canonical_type: "WireU64",
3754 size: 8,
3755 offset: 48,
3756 intent: FieldIntent::Custom,
3757 },
3758 FieldDescriptor {
3759 name: "bump",
3760 canonical_type: "u8",
3761 size: 1,
3762 offset: 56,
3763 intent: FieldIntent::Custom,
3764 },
3765 ];
3766
3767 const V1_MANIFEST: LayoutManifest = LayoutManifest {
3768 name: "Vault",
3769 disc: 1,
3770 version: 1,
3771 layout_id: [1, 2, 3, 4, 5, 6, 7, 8],
3772 total_size: 56,
3773 field_count: 2,
3774 fields: V1_FIELDS,
3775 };
3776
3777 const V2_MANIFEST: LayoutManifest = LayoutManifest {
3778 name: "Vault",
3779 disc: 1,
3780 version: 2,
3781 layout_id: [10, 20, 30, 40, 50, 60, 70, 80],
3782 total_size: 57,
3783 field_count: 3,
3784 fields: V2_FIELDS,
3785 };
3786
3787 #[test]
3788 fn no_op_for_identical() {
3789 let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &V1_MANIFEST);
3790 assert_eq!(plan.policy, MigrationPolicy::NoOp);
3791 assert_eq!(plan.step_count, 0);
3792 }
3793
3794 #[test]
3795 fn append_only_migration() {
3796 let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &V2_MANIFEST);
3797 assert_eq!(plan.policy, MigrationPolicy::AppendOnly);
3798 assert!(plan.step_count >= 3); assert_eq!(plan.old_size, 56);
3800 assert_eq!(plan.new_size, 57);
3801 assert!(plan.copy_bytes > 0);
3802 assert!(plan.zero_bytes > 0);
3803
3804 assert_eq!(plan.steps[0].action, MigrationAction::CopyPrefix);
3806 let mut found_zero = false;
3808 let mut i = 0;
3809 while i < plan.step_count {
3810 if plan.steps[i].action == MigrationAction::ZeroInit {
3811 assert_eq!(plan.steps[i].field, "bump");
3812 assert_eq!(plan.steps[i].size, 1);
3813 found_zero = true;
3814 }
3815 i += 1;
3816 }
3817 assert!(found_zero);
3818 }
3819
3820 #[test]
3821 fn incompatible_different_disc() {
3822 let other = LayoutManifest {
3823 disc: 99,
3824 ..V2_MANIFEST
3825 };
3826 let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &other);
3827 assert_eq!(plan.policy, MigrationPolicy::Incompatible);
3828 }
3829
3830 #[test]
3831 fn breaking_change_detected() {
3832 let changed_fields: &[FieldDescriptor] = &[
3833 FieldDescriptor {
3834 name: "authority",
3835 canonical_type: "WireU64",
3836 size: 8,
3837 offset: 16,
3838 intent: FieldIntent::Custom,
3839 },
3840 FieldDescriptor {
3841 name: "balance",
3842 canonical_type: "WireU64",
3843 size: 8,
3844 offset: 24,
3845 intent: FieldIntent::Custom,
3846 },
3847 ];
3848 let breaking = LayoutManifest {
3849 name: "Vault",
3850 disc: 1,
3851 version: 2,
3852 layout_id: [99; 8],
3853 total_size: 32,
3854 field_count: 2,
3855 fields: changed_fields,
3856 };
3857 let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &breaking);
3858 assert_eq!(plan.policy, MigrationPolicy::RequiresMigration);
3859 }
3860
3861 #[test]
3866 fn verdict_identical() {
3867 let v = CompatibilityVerdict::between(&V1_MANIFEST, &V1_MANIFEST);
3868 assert_eq!(v, CompatibilityVerdict::Identical);
3869 assert!(v.is_safe());
3870 assert!(v.is_backward_readable());
3871 assert!(!v.requires_migration());
3872 }
3873
3874 #[test]
3875 fn verdict_append_safe() {
3876 let v = CompatibilityVerdict::between(&V1_MANIFEST, &V2_MANIFEST);
3877 assert_eq!(v, CompatibilityVerdict::AppendSafe);
3878 assert!(v.is_safe());
3879 assert!(v.is_backward_readable());
3880 assert!(!v.requires_migration());
3881 }
3882
3883 #[test]
3884 fn verdict_migration_required() {
3885 let changed_fields: &[FieldDescriptor] = &[
3886 FieldDescriptor {
3887 name: "authority",
3888 canonical_type: "WireU64",
3889 size: 8,
3890 offset: 16,
3891 intent: FieldIntent::Custom,
3892 },
3893 FieldDescriptor {
3894 name: "balance",
3895 canonical_type: "WireU64",
3896 size: 8,
3897 offset: 24,
3898 intent: FieldIntent::Custom,
3899 },
3900 ];
3901 let breaking = LayoutManifest {
3902 name: "Vault",
3903 disc: 1,
3904 version: 2,
3905 layout_id: [99; 8],
3906 total_size: 32,
3907 field_count: 2,
3908 fields: changed_fields,
3909 };
3910 let v = CompatibilityVerdict::between(&V1_MANIFEST, &breaking);
3911 assert_eq!(v, CompatibilityVerdict::MigrationRequired);
3912 assert!(!v.is_safe());
3913 assert!(!v.is_backward_readable());
3914 assert!(v.requires_migration());
3915 }
3916
3917 #[test]
3918 fn verdict_wire_compatible() {
3919 let semantic_variant = LayoutManifest {
3921 layout_id: [77; 8], ..V1_MANIFEST };
3924 let v = CompatibilityVerdict::between(&V1_MANIFEST, &semantic_variant);
3925 assert_eq!(v, CompatibilityVerdict::WireCompatible);
3926 assert!(v.is_safe());
3927 assert!(v.is_backward_readable());
3928 assert!(!v.requires_migration());
3929 }
3930
3931 #[test]
3932 fn verdict_incompatible() {
3933 let other = LayoutManifest {
3934 disc: 99,
3935 ..V2_MANIFEST
3936 };
3937 let v = CompatibilityVerdict::between(&V1_MANIFEST, &other);
3938 assert_eq!(v, CompatibilityVerdict::Incompatible);
3939 assert!(!v.is_safe());
3940 }
3941
3942 #[test]
3943 fn verdict_names() {
3944 assert_eq!(CompatibilityVerdict::Identical.name(), "identical");
3945 assert_eq!(
3946 CompatibilityVerdict::WireCompatible.name(),
3947 "wire-compatible"
3948 );
3949 assert_eq!(CompatibilityVerdict::AppendSafe.name(), "append-safe");
3950 assert_eq!(
3951 CompatibilityVerdict::MigrationRequired.name(),
3952 "migration-required"
3953 );
3954 assert_eq!(CompatibilityVerdict::Incompatible.name(), "incompatible");
3955 }
3956
3957 #[test]
3958 fn segment_advice_core_must_preserve() {
3959 let segs = [DecodedSegment {
3960 id: [1, 0, 0, 0],
3961 offset: 36,
3962 size: 100,
3963 flags: 0x0000, version: 1,
3965 }];
3966 let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
3967 assert_eq!(report.count, 1);
3968 assert_eq!(report.advice[0].role, SegmentRoleHint::Core);
3969 assert!(report.advice[0].must_preserve);
3970 assert!(!report.advice[0].clearable);
3971 assert_eq!(report.preserve_bytes, 100);
3972 }
3973
3974 #[test]
3975 fn segment_advice_journal_clearable() {
3976 let segs = [DecodedSegment {
3977 id: [2, 0, 0, 0],
3978 offset: 136,
3979 size: 256,
3980 flags: 0x2000, version: 1,
3982 }];
3983 let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
3984 assert_eq!(report.advice[0].role, SegmentRoleHint::Journal);
3985 assert!(report.advice[0].clearable);
3986 assert!(report.advice[0].append_only);
3987 assert!(!report.advice[0].must_preserve);
3988 assert_eq!(report.clearable_bytes, 256);
3989 }
3990
3991 #[test]
3992 fn segment_advice_cache_rebuildable() {
3993 let segs = [DecodedSegment {
3994 id: [3, 0, 0, 0],
3995 offset: 400,
3996 size: 64,
3997 flags: 0x4000, version: 1,
3999 }];
4000 let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
4001 assert_eq!(report.advice[0].role, SegmentRoleHint::Cache);
4002 assert!(report.advice[0].clearable);
4003 assert!(report.advice[0].rebuildable);
4004 }
4005
4006 #[test]
4007 fn segment_advice_audit_immutable() {
4008 let segs = [DecodedSegment {
4009 id: [4, 0, 0, 0],
4010 offset: 200,
4011 size: 32,
4012 flags: 0x5000, version: 1,
4014 }];
4015 let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
4016 assert_eq!(report.advice[0].role, SegmentRoleHint::Audit);
4017 assert!(report.advice[0].must_preserve);
4018 assert!(report.advice[0].immutable);
4019 assert!(report.advice[0].append_only);
4020 assert!(!report.advice[0].clearable);
4021 }
4022
4023 #[test]
4024 fn segment_advice_mixed_report() {
4025 let segs = [
4026 DecodedSegment {
4027 id: [1, 0, 0, 0],
4028 offset: 36,
4029 size: 100,
4030 flags: 0x0000,
4031 version: 1,
4032 },
4033 DecodedSegment {
4034 id: [2, 0, 0, 0],
4035 offset: 136,
4036 size: 200,
4037 flags: 0x2000,
4038 version: 1,
4039 },
4040 DecodedSegment {
4041 id: [3, 0, 0, 0],
4042 offset: 336,
4043 size: 64,
4044 flags: 0x4000,
4045 version: 1,
4046 },
4047 ];
4048 let report = SegmentMigrationReport::<8>::analyze(&segs, 3);
4049 assert_eq!(report.count, 3);
4050 assert_eq!(report.must_preserve_count(), 1);
4051 assert_eq!(report.clearable_count(), 2);
4052 assert_eq!(report.preserve_bytes, 100);
4053 assert_eq!(report.clearable_bytes, 264);
4054 assert_eq!(report.rebuildable_bytes, 64);
4055 }
4056
4057 #[test]
4058 fn segment_role_hint_requires_migration_copy() {
4059 assert!(SegmentRoleHint::Core.requires_migration_copy());
4060 assert!(SegmentRoleHint::Audit.requires_migration_copy());
4061 assert!(!SegmentRoleHint::Extension.requires_migration_copy());
4062 assert!(!SegmentRoleHint::Journal.requires_migration_copy());
4063 assert!(!SegmentRoleHint::Index.requires_migration_copy());
4064 assert!(!SegmentRoleHint::Cache.requires_migration_copy());
4065 assert!(!SegmentRoleHint::Shard.requires_migration_copy());
4066 }
4067
4068 #[test]
4069 fn segment_role_hint_is_safe_to_drop() {
4070 assert!(SegmentRoleHint::Cache.is_safe_to_drop());
4071 assert!(!SegmentRoleHint::Core.is_safe_to_drop());
4072 assert!(!SegmentRoleHint::Extension.is_safe_to_drop());
4073 assert!(!SegmentRoleHint::Journal.is_safe_to_drop());
4074 assert!(!SegmentRoleHint::Index.is_safe_to_drop());
4075 assert!(!SegmentRoleHint::Audit.is_safe_to_drop());
4076 assert!(!SegmentRoleHint::Shard.is_safe_to_drop());
4077 }
4078
4079 static PM_LAYOUTS: &[LayoutManifest] = &[
4084 LayoutManifest {
4085 name: "Vault",
4086 disc: 1,
4087 version: 1,
4088 layout_id: [1, 2, 3, 4, 5, 6, 7, 8],
4089 total_size: 57,
4090 field_count: 0,
4091 fields: &[],
4092 },
4093 LayoutManifest {
4094 name: "Config",
4095 disc: 2,
4096 version: 1,
4097 layout_id: [8, 7, 6, 5, 4, 3, 2, 1],
4098 total_size: 43,
4099 field_count: 0,
4100 fields: &[],
4101 },
4102 ];
4103
4104 static PM_INSTRUCTIONS: &[InstructionDescriptor] = &[
4105 InstructionDescriptor {
4106 name: "deposit",
4107 tag: 1,
4108 args: &[],
4109 accounts: &[],
4110 capabilities: &["MutatesState"],
4111 policy_pack: "TREASURY_WRITE",
4112 receipt_expected: true,
4113 },
4114 InstructionDescriptor {
4115 name: "withdraw",
4116 tag: 2,
4117 args: &[],
4118 accounts: &[],
4119 capabilities: &["MutatesState", "TransfersTokens"],
4120 policy_pack: "TREASURY_WRITE",
4121 receipt_expected: true,
4122 },
4123 ];
4124
4125 static PM_POLICIES: &[PolicyDescriptor] = &[PolicyDescriptor {
4126 name: "TREASURY_WRITE",
4127 capabilities: &["MutatesState"],
4128 requirements: &["SignerAuthority"],
4129 invariants: &[],
4130 receipt_profile: "default-mutation",
4131 }];
4132
4133 #[test]
4134 fn program_manifest_find_layout_by_disc() {
4135 let prog = ProgramManifest {
4136 name: "test",
4137 version: "0.1.0",
4138 description: "",
4139 layouts: PM_LAYOUTS,
4140 layout_metadata: &[],
4141 instructions: &[],
4142 events: &[],
4143 policies: &[],
4144 compatibility_pairs: &[],
4145 tooling_hints: &[],
4146 contexts: &[],
4147 };
4148 assert_eq!(prog.layout_count(), 2);
4149 assert!(prog.find_layout_by_disc(1).is_some());
4150 assert_eq!(prog.find_layout_by_disc(1).unwrap().name, "Vault");
4151 assert!(prog.find_layout_by_disc(2).is_some());
4152 assert!(prog.find_layout_by_disc(3).is_none());
4153 }
4154
4155 #[test]
4156 fn program_manifest_find_layout_by_id() {
4157 let prog = ProgramManifest {
4158 name: "test",
4159 version: "0.1.0",
4160 description: "",
4161 layouts: PM_LAYOUTS,
4162 layout_metadata: &[],
4163 instructions: &[],
4164 events: &[],
4165 policies: &[],
4166 compatibility_pairs: &[],
4167 tooling_hints: &[],
4168 contexts: &[],
4169 };
4170 let id = [1, 2, 3, 4, 5, 6, 7, 8];
4171 assert!(prog.find_layout_by_id(&id).is_some());
4172 let bad_id = [0, 0, 0, 0, 0, 0, 0, 0];
4173 assert!(prog.find_layout_by_id(&bad_id).is_none());
4174 }
4175
4176 #[test]
4177 fn program_manifest_identify_from_data() {
4178 static ID_LAYOUTS: &[LayoutManifest] = &[LayoutManifest {
4179 name: "Vault",
4180 disc: 1,
4181 version: 1,
4182 layout_id: [0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80],
4183 total_size: 57,
4184 field_count: 0,
4185 fields: &[],
4186 }];
4187 let prog = ProgramManifest {
4188 name: "test",
4189 version: "0.1.0",
4190 description: "",
4191 layouts: ID_LAYOUTS,
4192 layout_metadata: &[],
4193 instructions: &[],
4194 events: &[],
4195 policies: &[],
4196 compatibility_pairs: &[],
4197 tooling_hints: &[],
4198 contexts: &[],
4199 };
4200 let mut data = [0u8; 57];
4202 data[0] = 1; data[1] = 1; data[4..12].copy_from_slice(&[0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80]);
4205 let result = prog.identify_from_data(&data);
4206 assert!(result.is_some());
4207 assert_eq!(result.unwrap().name, "Vault");
4208 }
4209
4210 #[test]
4211 fn program_manifest_find_instruction() {
4212 let prog = ProgramManifest {
4213 name: "test",
4214 version: "0.1.0",
4215 description: "",
4216 layouts: &[],
4217 layout_metadata: &[],
4218 instructions: PM_INSTRUCTIONS,
4219 events: &[],
4220 policies: &[],
4221 compatibility_pairs: &[],
4222 tooling_hints: &[],
4223 contexts: &[],
4224 };
4225 assert_eq!(prog.instruction_count(), 2);
4226 assert_eq!(prog.find_instruction(1).unwrap().name, "deposit");
4227 assert_eq!(prog.find_instruction(2).unwrap().name, "withdraw");
4228 assert!(prog.find_instruction(3).is_none());
4229 }
4230
4231 #[test]
4232 fn program_manifest_find_policy() {
4233 let prog = ProgramManifest {
4234 name: "test",
4235 version: "0.1.0",
4236 description: "",
4237 layouts: &[],
4238 layout_metadata: &[],
4239 instructions: &[],
4240 events: &[],
4241 policies: PM_POLICIES,
4242 compatibility_pairs: &[],
4243 tooling_hints: &[],
4244 contexts: &[],
4245 };
4246 assert!(prog.find_policy("TREASURY_WRITE").is_some());
4247 assert!(prog.find_policy("NONEXISTENT").is_none());
4248 }
4249
4250 #[test]
4251 fn decode_account_fields_basic() {
4252 static DECODE_FIELDS: &[FieldDescriptor] = &[
4253 FieldDescriptor {
4254 name: "balance",
4255 canonical_type: "WireU64",
4256 size: 8,
4257 offset: 16,
4258 intent: FieldIntent::Custom,
4259 },
4260 FieldDescriptor {
4261 name: "bump",
4262 canonical_type: "u8",
4263 size: 1,
4264 offset: 24,
4265 intent: FieldIntent::Custom,
4266 },
4267 ];
4268 static DECODE_MANIFEST: LayoutManifest = LayoutManifest {
4269 name: "Test",
4270 disc: 1,
4271 version: 1,
4272 layout_id: [0; 8],
4273 total_size: 25,
4274 field_count: 2,
4275 fields: DECODE_FIELDS,
4276 };
4277 let mut data = [0u8; 25];
4278 let balance_bytes = 1000u64.to_le_bytes();
4279 data[16..24].copy_from_slice(&balance_bytes);
4280 data[24] = 254;
4281
4282 let (count, decoded) = decode_account_fields::<8>(&data, &DECODE_MANIFEST);
4283 assert_eq!(count, 2);
4284 assert!(decoded[0].is_some());
4285 assert_eq!(decoded[0].as_ref().unwrap().name, "balance");
4286 assert!(decoded[1].is_some());
4287 assert_eq!(decoded[1].as_ref().unwrap().name, "bump");
4288 assert_eq!(decoded[1].as_ref().unwrap().raw[0], 254);
4289 }
4290
4291 #[test]
4292 fn decoded_field_format_wire_u64() {
4293 let raw = 42u64.to_le_bytes();
4294 let field = DecodedField {
4295 name: "balance",
4296 canonical_type: "WireU64",
4297 raw: &raw,
4298 offset: 16,
4299 size: 8,
4300 };
4301 let mut buf = [0u8; 32];
4302 let len = field.format_value(&mut buf);
4303 assert_eq!(&buf[..len], b"42");
4304 }
4305
4306 #[test]
4307 fn decoded_field_format_wire_u32() {
4308 let raw = 65535u32.to_le_bytes();
4309 let field = DecodedField {
4310 name: "count",
4311 canonical_type: "WireU32",
4312 raw: &raw,
4313 offset: 0,
4314 size: 4,
4315 };
4316 let mut buf = [0u8; 32];
4317 let len = field.format_value(&mut buf);
4318 assert_eq!(&buf[..len], b"65535");
4319 }
4320
4321 #[test]
4322 fn decoded_field_format_bool() {
4323 let raw_true = [1u8];
4324 let field = DecodedField {
4325 name: "frozen",
4326 canonical_type: "WireBool",
4327 raw: &raw_true,
4328 offset: 0,
4329 size: 1,
4330 };
4331 let mut buf = [0u8; 32];
4332 let len = field.format_value(&mut buf);
4333 assert_eq!(&buf[..len], b"true");
4334
4335 let raw_false = [0u8];
4336 let field2 = DecodedField {
4337 name: "frozen",
4338 canonical_type: "WireBool",
4339 raw: &raw_false,
4340 offset: 0,
4341 size: 1,
4342 };
4343 let len = field2.format_value(&mut buf);
4344 assert_eq!(&buf[..len], b"false");
4345 }
4346
4347 #[test]
4348 fn decoded_field_format_address() {
4349 let raw = [0xABu8; 32];
4350 let field = DecodedField {
4351 name: "authority",
4352 canonical_type: "[u8;32]",
4353 raw: &raw,
4354 offset: 0,
4355 size: 32,
4356 };
4357 let mut buf = [0u8; 64];
4358 let len = field.format_value(&mut buf);
4359 let s = core::str::from_utf8(&buf[..len]).unwrap();
4360 assert!(s.starts_with("0x"));
4361 assert!(s.ends_with("..."));
4362 }
4363
4364 #[test]
4365 fn format_u64_basic() {
4366 let mut buf = [0u8; 32];
4367 let len = super::format_u64(12345, &mut buf);
4368 assert_eq!(&buf[..len], b"12345");
4369
4370 let len = super::format_u64(0, &mut buf);
4371 assert_eq!(&buf[..len], b"0");
4372
4373 let len = super::format_u64(u64::MAX, &mut buf);
4374 let expected = b"18446744073709551615";
4375 assert_eq!(&buf[..len], &expected[..]);
4376 }
4377
4378 #[test]
4379 fn format_hex_truncated_short() {
4380 let mut buf = [0u8; 64];
4381 let len = super::format_hex_truncated(&[0xAB, 0xCD], &mut buf);
4382 assert_eq!(&buf[..len], b"0xabcd");
4383 }
4384
4385 #[test]
4386 fn format_hex_truncated_long() {
4387 let mut buf = [0u8; 64];
4388 let data = [0xFFu8; 32];
4389 let len = super::format_hex_truncated(&data, &mut buf);
4390 let s = core::str::from_utf8(&buf[..len]).unwrap();
4391 assert!(s.starts_with("0x"));
4392 assert!(s.ends_with("..."));
4393 assert_eq!(len, 21); }
4395
4396 #[test]
4397 fn program_manifest_display() {
4398 let prog = ProgramManifest {
4399 name: "test_program",
4400 version: "0.1.0",
4401 description: "A test",
4402 layouts: PM_LAYOUTS,
4403 layout_metadata: &[],
4404 instructions: PM_INSTRUCTIONS,
4405 events: &[],
4406 policies: PM_POLICIES,
4407 compatibility_pairs: &[],
4408 tooling_hints: &[],
4409 contexts: &[],
4410 };
4411 extern crate alloc;
4412 use alloc::format;
4413 let s = format!("{}", prog);
4414 assert!(s.contains("test_program"));
4415 assert!(s.contains("Vault"));
4416 assert!(s.contains("deposit"));
4417 assert!(s.contains("MutatesState"));
4418 assert!(s.contains("TREASURY_WRITE"));
4419 assert!(s.contains("SignerAuthority"));
4420 }
4421
4422 #[test]
4423 fn program_manifest_empty() {
4424 let prog = ProgramManifest::empty();
4425 assert_eq!(prog.layout_count(), 0);
4426 assert_eq!(prog.instruction_count(), 0);
4427 assert!(prog.find_layout_by_disc(0).is_none());
4428 assert!(prog.find_instruction(0).is_none());
4429 assert!(prog.identify_from_data(&[0u8; 16]).is_none());
4430 }
4431
4432 #[test]
4437 fn decode_header_empty_buffer() {
4438 assert!(decode_header(&[]).is_none());
4439 }
4440
4441 #[test]
4442 fn decode_header_one_byte() {
4443 assert!(decode_header(&[0xFF]).is_none());
4444 }
4445
4446 #[test]
4447 fn decode_header_fifteen_bytes() {
4448 assert!(decode_header(&[0u8; 15]).is_none());
4449 }
4450
4451 #[test]
4452 fn decode_header_exact_sixteen() {
4453 let h = decode_header(&[0u8; 16]);
4454 assert!(h.is_some());
4455 let h = h.unwrap();
4456 assert_eq!(h.disc, 0);
4457 assert_eq!(h.version, 0);
4458 }
4459
4460 #[test]
4461 fn decode_header_large_buffer() {
4462 let data = [0xABu8; 1024];
4463 let h = decode_header(&data).unwrap();
4464 assert_eq!(h.disc, 0xAB);
4465 assert_eq!(h.version, 0xAB);
4466 }
4467
4468 #[test]
4469 fn decode_segments_too_short() {
4470 assert!(decode_segments::<8>(&[0u8; 19]).is_none());
4472 }
4473
4474 #[test]
4475 fn decode_segments_zero_count() {
4476 let mut data = [0u8; 20];
4478 data[16] = 0; data[17] = 0; let result = decode_segments::<8>(&data);
4481 assert!(result.is_some());
4482 let (n, _) = result.unwrap();
4483 assert_eq!(n, 0);
4484 }
4485
4486 #[test]
4487 fn compare_fields_identical_empty() {
4488 let a = LayoutManifest {
4489 name: "A",
4490 disc: 1,
4491 version: 1,
4492 layout_id: [0; 8],
4493 total_size: 16,
4494 field_count: 0,
4495 fields: &[],
4496 };
4497 let b = LayoutManifest {
4498 name: "B",
4499 disc: 1,
4500 version: 1,
4501 layout_id: [0; 8],
4502 total_size: 16,
4503 field_count: 0,
4504 fields: &[],
4505 };
4506 let report = compare_fields::<8>(&a, &b);
4507 assert_eq!(report.count, 0);
4508 assert!(report.is_append_safe);
4509 }
4510
4511 static SINGLE_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4512 name: "x",
4513 canonical_type: "u8",
4514 size: 1,
4515 offset: 16,
4516 intent: FieldIntent::Custom,
4517 }];
4518
4519 #[test]
4520 fn compare_fields_all_removed() {
4521 let a = LayoutManifest {
4522 name: "A",
4523 disc: 1,
4524 version: 1,
4525 layout_id: [1; 8],
4526 total_size: 17,
4527 field_count: 1,
4528 fields: SINGLE_FIELD,
4529 };
4530 let b = LayoutManifest {
4531 name: "B",
4532 disc: 1,
4533 version: 2,
4534 layout_id: [2; 8],
4535 total_size: 16,
4536 field_count: 0,
4537 fields: &[],
4538 };
4539 let report = compare_fields::<8>(&a, &b);
4540 assert_eq!(report.count, 1);
4541 assert!(!report.is_append_safe);
4542 }
4543
4544 static OLD_TYPE_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4545 name: "x",
4546 canonical_type: "u8",
4547 size: 1,
4548 offset: 16,
4549 intent: FieldIntent::Custom,
4550 }];
4551 static NEW_TYPE_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4552 name: "x",
4553 canonical_type: "u16",
4554 size: 2,
4555 offset: 16,
4556 intent: FieldIntent::Custom,
4557 }];
4558
4559 #[test]
4560 fn compare_fields_type_change_detected() {
4561 let a = LayoutManifest {
4562 name: "A",
4563 disc: 1,
4564 version: 1,
4565 layout_id: [1; 8],
4566 total_size: 17,
4567 field_count: 1,
4568 fields: OLD_TYPE_FIELD,
4569 };
4570 let b = LayoutManifest {
4571 name: "B",
4572 disc: 1,
4573 version: 2,
4574 layout_id: [2; 8],
4575 total_size: 18,
4576 field_count: 1,
4577 fields: NEW_TYPE_FIELD,
4578 };
4579 let report = compare_fields::<8>(&a, &b);
4580 assert_eq!(report.entries[0].status, FieldCompat::Changed);
4581 assert!(!report.is_append_safe);
4582 }
4583
4584 #[test]
4585 fn verdict_different_disc_is_incompatible() {
4586 let a = LayoutManifest {
4587 name: "A",
4588 disc: 1,
4589 version: 1,
4590 layout_id: [1; 8],
4591 total_size: 16,
4592 field_count: 0,
4593 fields: &[],
4594 };
4595 let b = LayoutManifest {
4596 name: "B",
4597 disc: 2,
4598 version: 1,
4599 layout_id: [2; 8],
4600 total_size: 16,
4601 field_count: 0,
4602 fields: &[],
4603 };
4604 assert_eq!(
4605 CompatibilityVerdict::between(&a, &b),
4606 CompatibilityVerdict::Incompatible
4607 );
4608 }
4609
4610 #[test]
4611 fn verdict_same_id_is_identical() {
4612 let a = LayoutManifest {
4613 name: "A",
4614 disc: 1,
4615 version: 1,
4616 layout_id: [9; 8],
4617 total_size: 16,
4618 field_count: 0,
4619 fields: &[],
4620 };
4621 assert_eq!(
4622 CompatibilityVerdict::between(&a, &a),
4623 CompatibilityVerdict::Identical
4624 );
4625 }
4626
4627 #[test]
4628 fn compatibility_explain_between_identical() {
4629 let a = LayoutManifest {
4630 name: "A",
4631 disc: 1,
4632 version: 1,
4633 layout_id: [9; 8],
4634 total_size: 16,
4635 field_count: 0,
4636 fields: &[],
4637 };
4638 let exp = CompatibilityExplain::between(&a, &a);
4639 assert_eq!(exp.verdict, CompatibilityVerdict::Identical);
4640 assert_eq!(exp.added_count, 0);
4641 assert_eq!(exp.removed_count, 0);
4642 assert!(!exp.semantic_drift);
4643 }
4644
4645 static APPEND_OLD: &[FieldDescriptor] = &[FieldDescriptor {
4646 name: "a",
4647 canonical_type: "u8",
4648 size: 1,
4649 offset: 16,
4650 intent: FieldIntent::Custom,
4651 }];
4652 static APPEND_NEW: &[FieldDescriptor] = &[
4653 FieldDescriptor {
4654 name: "a",
4655 canonical_type: "u8",
4656 size: 1,
4657 offset: 16,
4658 intent: FieldIntent::Custom,
4659 },
4660 FieldDescriptor {
4661 name: "b",
4662 canonical_type: "u8",
4663 size: 1,
4664 offset: 17,
4665 intent: FieldIntent::Custom,
4666 },
4667 ];
4668
4669 #[test]
4670 fn compatibility_explain_append_counts_fields() {
4671 let older = LayoutManifest {
4672 name: "T",
4673 disc: 1,
4674 version: 1,
4675 layout_id: [1; 8],
4676 total_size: 17,
4677 field_count: 1,
4678 fields: APPEND_OLD,
4679 };
4680 let newer = LayoutManifest {
4681 name: "T",
4682 disc: 1,
4683 version: 2,
4684 layout_id: [2; 8],
4685 total_size: 18,
4686 field_count: 2,
4687 fields: APPEND_NEW,
4688 };
4689 let exp = CompatibilityExplain::between(&older, &newer);
4690 assert_eq!(exp.verdict, CompatibilityVerdict::AppendSafe);
4691 assert_eq!(exp.added_count, 1);
4692 assert_eq!(exp.added_fields[0], "b");
4693 }
4694
4695 #[test]
4696 fn layout_fingerprint_deterministic() {
4697 let m = LayoutManifest {
4698 name: "X",
4699 disc: 1,
4700 version: 1,
4701 layout_id: [5; 8],
4702 total_size: 16,
4703 field_count: 0,
4704 fields: &[],
4705 };
4706 let fp1 = LayoutFingerprint::from_manifest(&m);
4707 let fp2 = LayoutFingerprint::from_manifest(&m);
4708 assert_eq!(fp1.wire_hash, fp2.wire_hash);
4709 assert_eq!(fp1.semantic_hash, fp2.semantic_hash);
4710 }
4711
4712 static FP_CUSTOM: &[FieldDescriptor] = &[FieldDescriptor {
4713 name: "x",
4714 canonical_type: "u8",
4715 size: 1,
4716 offset: 16,
4717 intent: FieldIntent::Custom,
4718 }];
4719 static FP_BALANCE: &[FieldDescriptor] = &[FieldDescriptor {
4720 name: "x",
4721 canonical_type: "u8",
4722 size: 1,
4723 offset: 16,
4724 intent: FieldIntent::Balance,
4725 }];
4726
4727 #[test]
4728 fn layout_fingerprint_differs_on_intent_change() {
4729 let m1 = LayoutManifest {
4730 name: "T",
4731 disc: 1,
4732 version: 1,
4733 layout_id: [1; 8],
4734 total_size: 17,
4735 field_count: 1,
4736 fields: FP_CUSTOM,
4737 };
4738 let m2 = LayoutManifest {
4739 name: "T",
4740 disc: 1,
4741 version: 1,
4742 layout_id: [1; 8],
4743 total_size: 17,
4744 field_count: 1,
4745 fields: FP_BALANCE,
4746 };
4747 let fp1 = LayoutFingerprint::from_manifest(&m1);
4748 let fp2 = LayoutFingerprint::from_manifest(&m2);
4749 assert_eq!(fp1.wire_hash, fp2.wire_hash);
4750 assert_ne!(fp1.semantic_hash, fp2.semantic_hash);
4751 }
4752
4753 static LINT_AUTH_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4754 name: "auth",
4755 canonical_type: "[u8;32]",
4756 size: 32,
4757 offset: 16,
4758 intent: FieldIntent::Authority,
4759 }];
4760
4761 #[test]
4762 fn lint_layout_authority_without_signer() {
4763 let m = LayoutManifest {
4764 name: "T",
4765 disc: 1,
4766 version: 1,
4767 layout_id: [0; 8],
4768 total_size: 48,
4769 field_count: 1,
4770 fields: LINT_AUTH_FIELD,
4771 };
4772 let behavior = LayoutBehavior {
4774 requires_signer: false,
4775 affects_balance: false,
4776 affects_authority: true,
4777 mutation_class: MutationClass::InPlace,
4778 };
4779 let (n, lints) = lint_layout::<8>(&m, &behavior);
4780 assert!(n >= 1);
4781 assert_eq!(lints[0].code, "E001");
4782 }
4783
4784 #[test]
4785 fn lint_layout_clean_passes() {
4786 let m = LayoutManifest {
4787 name: "T",
4788 disc: 1,
4789 version: 1,
4790 layout_id: [0; 8],
4791 total_size: 48,
4792 field_count: 1,
4793 fields: LINT_AUTH_FIELD,
4794 };
4795 let behavior = LayoutBehavior {
4796 requires_signer: true,
4797 affects_balance: false,
4798 affects_authority: true,
4799 mutation_class: MutationClass::AuthoritySensitive,
4800 };
4801 let (n, _) = lint_layout::<8>(&m, &behavior);
4802 assert_eq!(n, 0);
4803 }
4804
4805 #[test]
4806 fn mutation_class_properties() {
4807 assert!(!MutationClass::ReadOnly.is_mutating());
4808 assert!(MutationClass::InPlace.is_mutating());
4809 assert!(MutationClass::Financial.requires_snapshot());
4810 assert!(MutationClass::AuthoritySensitive.requires_authority());
4811 assert!(!MutationClass::AppendOnly.requires_authority());
4812 }
4813
4814 static SEED_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4815 name: "seed",
4816 canonical_type: "[u8;32]",
4817 size: 32,
4818 offset: 16,
4819 intent: FieldIntent::PDASeed,
4820 }];
4821
4822 #[test]
4823 fn layout_stability_grade_stable_with_init_only() {
4824 let m = LayoutManifest {
4825 name: "T",
4826 disc: 1,
4827 version: 1,
4828 layout_id: [0; 8],
4829 total_size: 48,
4830 field_count: 1,
4831 fields: SEED_FIELD,
4832 };
4833 assert_eq!(
4834 LayoutStabilityGrade::compute(&m),
4835 LayoutStabilityGrade::Stable
4836 );
4837 }
4838
4839 #[test]
4840 fn layout_stability_grade_evolving_with_custom() {
4841 let m = LayoutManifest {
4842 name: "T",
4843 disc: 1,
4844 version: 1,
4845 layout_id: [0; 8],
4846 total_size: 17,
4847 field_count: 1,
4848 fields: SINGLE_FIELD,
4849 };
4850 assert_eq!(
4851 LayoutStabilityGrade::compute(&m),
4852 LayoutStabilityGrade::Evolving
4853 );
4854 }
4855
4856 static GRADE_HEAVY: &[FieldDescriptor] = &[
4857 FieldDescriptor {
4858 name: "auth1",
4859 canonical_type: "[u8;32]",
4860 size: 32,
4861 offset: 16,
4862 intent: FieldIntent::Authority,
4863 },
4864 FieldDescriptor {
4865 name: "auth2",
4866 canonical_type: "[u8;32]",
4867 size: 32,
4868 offset: 48,
4869 intent: FieldIntent::Owner,
4870 },
4871 FieldDescriptor {
4872 name: "auth3",
4873 canonical_type: "[u8;32]",
4874 size: 32,
4875 offset: 80,
4876 intent: FieldIntent::Delegate,
4877 },
4878 FieldDescriptor {
4879 name: "bal1",
4880 canonical_type: "WireU64",
4881 size: 8,
4882 offset: 112,
4883 intent: FieldIntent::Balance,
4884 },
4885 FieldDescriptor {
4886 name: "bal2",
4887 canonical_type: "WireU64",
4888 size: 8,
4889 offset: 120,
4890 intent: FieldIntent::Supply,
4891 },
4892 FieldDescriptor {
4893 name: "bal3",
4894 canonical_type: "WireU64",
4895 size: 8,
4896 offset: 128,
4897 intent: FieldIntent::Balance,
4898 },
4899 ];
4900
4901 #[test]
4902 fn layout_stability_grade_unsafe_to_evolve_heavy() {
4903 let m = LayoutManifest {
4904 name: "T",
4905 disc: 1,
4906 version: 1,
4907 layout_id: [0; 8],
4908 total_size: 136,
4909 field_count: 6,
4910 fields: GRADE_HEAVY,
4911 };
4912 let grade = LayoutStabilityGrade::compute(&m);
4913 assert_eq!(grade, LayoutStabilityGrade::UnsafeToEvolve);
4914 }
4915
4916 #[test]
4917 fn field_intent_new_variants_coverage() {
4918 assert_eq!(FieldIntent::PDASeed.name(), "pda_seed");
4919 assert_eq!(FieldIntent::Version.name(), "version");
4920 assert_eq!(FieldIntent::Bump.name(), "bump");
4921 assert_eq!(FieldIntent::Status.name(), "status");
4922 assert!(FieldIntent::Owner.is_authority_sensitive());
4923 assert!(FieldIntent::Delegate.is_authority_sensitive());
4924 assert!(FieldIntent::Threshold.is_governance());
4925 assert!(FieldIntent::Bump.is_init_only());
4926 assert!(FieldIntent::PDASeed.is_init_only());
4927 assert!(FieldIntent::Supply.is_monetary());
4928 }
4929
4930 #[test]
4931 fn refine_verdict_softens_with_rebuildable_segments() {
4932 let advice = [
4933 SegmentAdvice {
4934 id: [0; 4],
4935 size: 100,
4936 role: SegmentRoleHint::Cache,
4937 must_preserve: false,
4938 clearable: true,
4939 rebuildable: true,
4940 append_only: false,
4941 immutable: false,
4942 },
4943 SegmentAdvice {
4944 id: [0; 4],
4945 size: 0,
4946 role: SegmentRoleHint::Unclassified,
4947 must_preserve: false,
4948 clearable: false,
4949 rebuildable: false,
4950 append_only: false,
4951 immutable: false,
4952 },
4953 ];
4954 let report = SegmentMigrationReport {
4955 advice,
4956 count: 1,
4957 preserve_bytes: 0,
4958 clearable_bytes: 100,
4959 rebuildable_bytes: 100,
4960 };
4961 let refined = CompatibilityVerdict::MigrationRequired.refine_with_roles(&report);
4962 assert_eq!(refined, CompatibilityVerdict::AppendSafe);
4963 }
4964
4965 #[test]
4966 fn refine_verdict_escalates_with_immutable_segment() {
4967 let advice = [SegmentAdvice {
4968 id: [0; 4],
4969 size: 50,
4970 role: SegmentRoleHint::Audit,
4971 must_preserve: true,
4972 clearable: false,
4973 rebuildable: false,
4974 append_only: true,
4975 immutable: true,
4976 }];
4977 let report = SegmentMigrationReport {
4978 advice,
4979 count: 1,
4980 preserve_bytes: 50,
4981 clearable_bytes: 0,
4982 rebuildable_bytes: 0,
4983 };
4984 let refined = CompatibilityVerdict::AppendSafe.refine_with_roles(&report);
4985 assert_eq!(refined, CompatibilityVerdict::MigrationRequired);
4986 }
4987
4988 #[test]
4989 #[cfg(feature = "policy")]
4990 fn lint_policy_financial_mismatch() {
4991 let behavior = LayoutBehavior {
4992 requires_signer: true,
4993 affects_balance: true,
4994 affects_authority: false,
4995 mutation_class: MutationClass::Financial,
4996 };
4997 let (n, lints) = lint_policy::<8>(&behavior, hopper_core::policy::PolicyClass::Write);
4998 assert!(n >= 1);
4999 assert_eq!(lints[0].code, "W005");
5000 }
5001
5002 #[test]
5003 #[cfg(feature = "policy")]
5004 fn lint_policy_reverse_mismatch() {
5005 let behavior = LayoutBehavior {
5006 requires_signer: true,
5007 affects_balance: false,
5008 affects_authority: false,
5009 mutation_class: MutationClass::InPlace,
5010 };
5011 let (n, lints) = lint_policy::<8>(&behavior, hopper_core::policy::PolicyClass::Financial);
5012 assert!(n >= 1);
5013 assert_eq!(lints[0].code, "W006");
5014 }
5015
5016 #[test]
5017 #[cfg(feature = "policy")]
5018 fn lint_policy_clean_when_aligned() {
5019 let behavior = LayoutBehavior {
5020 requires_signer: true,
5021 affects_balance: true,
5022 affects_authority: false,
5023 mutation_class: MutationClass::Financial,
5024 };
5025 let (n, _) = lint_policy::<8>(&behavior, hopper_core::policy::PolicyClass::Financial);
5026 assert_eq!(n, 0);
5027 }
5028
5029 #[test]
5030 fn display_field_intent() {
5031 extern crate alloc;
5032 use alloc::format;
5033 assert_eq!(format!("{}", FieldIntent::Balance), "balance");
5034 assert_eq!(format!("{}", FieldIntent::Authority), "authority");
5035 }
5036
5037 #[test]
5038 fn display_mutation_class() {
5039 extern crate alloc;
5040 use alloc::format;
5041 assert_eq!(format!("{}", MutationClass::Financial), "financial");
5042 assert_eq!(format!("{}", MutationClass::ReadOnly), "read-only");
5043 }
5044
5045 #[test]
5046 fn display_layout_stability_grade() {
5047 extern crate alloc;
5048 use alloc::format;
5049 assert_eq!(format!("{}", LayoutStabilityGrade::Stable), "stable");
5050 assert_eq!(
5051 format!("{}", LayoutStabilityGrade::UnsafeToEvolve),
5052 "unsafe-to-evolve"
5053 );
5054 }
5055
5056 #[test]
5057 fn display_compatibility_verdict() {
5058 extern crate alloc;
5059 use alloc::format;
5060 assert_eq!(format!("{}", CompatibilityVerdict::Identical), "identical");
5061 assert_eq!(
5062 format!("{}", CompatibilityVerdict::MigrationRequired),
5063 "migration-required"
5064 );
5065 }
5066
5067 #[test]
5068 fn display_layout_fingerprint() {
5069 extern crate alloc;
5070 use alloc::format;
5071 let fp = LayoutFingerprint {
5072 wire_hash: [0xAB, 0xCD, 0, 0, 0, 0, 0, 0],
5073 semantic_hash: [0, 0, 0, 0, 0, 0, 0xFF, 0x01],
5074 };
5075 let s = format!("{}", fp);
5076 assert!(s.starts_with("wire=abcd"));
5077 assert!(s.contains("sem="));
5078 assert!(s.ends_with("ff01"));
5079 }
5080
5081 #[test]
5082 fn display_receipt_profile() {
5083 extern crate alloc;
5084 use alloc::format;
5085 let rp = ReceiptProfile {
5086 name: "test",
5087 expected_phase: "Mutate",
5088 expects_balance_change: true,
5089 expects_authority_change: false,
5090 expects_journal_append: false,
5091 min_changed_fields: 2,
5092 };
5093 let s = format!("{}", rp);
5094 assert!(s.contains("test"));
5095 assert!(s.contains("Mutate"));
5096 assert!(s.contains("balance"));
5097 assert!(s.contains("min_fields=2"));
5098 }
5099
5100 #[test]
5101 fn display_idl_segment_descriptor() {
5102 extern crate alloc;
5103 use alloc::format;
5104 let sd = IdlSegmentDescriptor {
5105 name: "core",
5106 role: "Core",
5107 append_only: false,
5108 rebuildable: false,
5109 must_preserve: true,
5110 };
5111 let s = format!("{}", sd);
5112 assert!(s.contains("core"));
5113 assert!(s.contains("Core"));
5114 assert!(s.contains("must-preserve"));
5115 assert!(!s.contains("append-only"));
5116 }
5117}