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)]
2057pub enum AccountResolverKind {
2058 Provided,
2060 Pda,
2062 Constant,
2064 Optional,
2066 Program,
2068 Sysvar,
2070}
2071
2072impl AccountResolverKind {
2073 pub const fn as_str(self) -> &'static str {
2075 match self {
2076 Self::Provided => "provided",
2077 Self::Pda => "pda",
2078 Self::Constant => "constant",
2079 Self::Optional => "optional",
2080 Self::Program => "program",
2081 Self::Sysvar => "sysvar",
2082 }
2083 }
2084}
2085
2086impl fmt::Display for AccountResolverKind {
2087 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2088 f.write_str(self.as_str())
2089 }
2090}
2091
2092#[derive(Clone, Copy, Debug)]
2094pub struct AccountResolverDescriptor {
2095 pub account: &'static str,
2097 pub kind: AccountResolverKind,
2099 pub seeds: &'static [&'static str],
2101 pub expected_address: &'static str,
2103 pub expected_owner: &'static str,
2105 pub payer: &'static str,
2107 pub layout_ref: &'static str,
2109 pub optional: bool,
2111}
2112
2113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2115pub enum InstructionEffectKind {
2116 Reads,
2118 Writes,
2120 CreatesAccount,
2122 ReallocatesAccount,
2124 ClosesAccount,
2126 RequiresSigner,
2128 EmitsReceipt,
2130 InvokesCpi,
2132}
2133
2134impl InstructionEffectKind {
2135 pub const fn as_str(self) -> &'static str {
2137 match self {
2138 Self::Reads => "reads",
2139 Self::Writes => "writes",
2140 Self::CreatesAccount => "creates_account",
2141 Self::ReallocatesAccount => "reallocates_account",
2142 Self::ClosesAccount => "closes_account",
2143 Self::RequiresSigner => "requires_signer",
2144 Self::EmitsReceipt => "emits_receipt",
2145 Self::InvokesCpi => "invokes_cpi",
2146 }
2147 }
2148}
2149
2150impl fmt::Display for InstructionEffectKind {
2151 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2152 f.write_str(self.as_str())
2153 }
2154}
2155
2156#[derive(Clone, Copy, Debug)]
2158pub struct InstructionEffectDescriptor {
2159 pub kind: InstructionEffectKind,
2161 pub target: &'static str,
2163 pub layout_ref: &'static str,
2165 pub reason: &'static str,
2167}
2168
2169impl crate::accounts::ContextAccountDescriptor {
2170 pub const fn resolver_kind(&self) -> AccountResolverKind {
2172 if self.expected_address.as_bytes().len() > 0 {
2173 AccountResolverKind::Constant
2174 } else if self.seeds.len() > 0 {
2175 AccountResolverKind::Pda
2176 } else if self.optional {
2177 AccountResolverKind::Optional
2178 } else if self.kind.as_bytes().len() >= 6
2179 && self.kind.as_bytes()[0] == b'S'
2180 && self.kind.as_bytes()[1] == b'y'
2181 && self.kind.as_bytes()[2] == b's'
2182 && self.kind.as_bytes()[3] == b'v'
2183 && self.kind.as_bytes()[4] == b'a'
2184 && self.kind.as_bytes()[5] == b'r'
2185 {
2186 AccountResolverKind::Sysvar
2187 } else if self.kind.as_bytes().len() >= 7
2188 && self.kind.as_bytes()[0] == b'P'
2189 && self.kind.as_bytes()[1] == b'r'
2190 && self.kind.as_bytes()[2] == b'o'
2191 && self.kind.as_bytes()[3] == b'g'
2192 && self.kind.as_bytes()[4] == b'r'
2193 && self.kind.as_bytes()[5] == b'a'
2194 && self.kind.as_bytes()[6] == b'm'
2195 {
2196 AccountResolverKind::Program
2197 } else {
2198 AccountResolverKind::Provided
2199 }
2200 }
2201
2202 pub const fn resolver_descriptor(&self) -> AccountResolverDescriptor {
2204 AccountResolverDescriptor {
2205 account: self.name,
2206 kind: self.resolver_kind(),
2207 seeds: self.seeds,
2208 expected_address: self.expected_address,
2209 expected_owner: self.expected_owner,
2210 payer: self.payer,
2211 layout_ref: self.layout_ref,
2212 optional: self.optional,
2213 }
2214 }
2215
2216 pub const fn primary_effect_kind(&self) -> InstructionEffectKind {
2218 match self.lifecycle {
2219 crate::accounts::AccountLifecycle::Init => InstructionEffectKind::CreatesAccount,
2220 crate::accounts::AccountLifecycle::Realloc => InstructionEffectKind::ReallocatesAccount,
2221 crate::accounts::AccountLifecycle::Close => InstructionEffectKind::ClosesAccount,
2222 crate::accounts::AccountLifecycle::Existing => {
2223 if self.writable {
2224 InstructionEffectKind::Writes
2225 } else if self.signer {
2226 InstructionEffectKind::RequiresSigner
2227 } else {
2228 InstructionEffectKind::Reads
2229 }
2230 }
2231 }
2232 }
2233
2234 pub const fn effect_descriptor(&self) -> InstructionEffectDescriptor {
2236 InstructionEffectDescriptor {
2237 kind: self.primary_effect_kind(),
2238 target: self.name,
2239 layout_ref: self.layout_ref,
2240 reason: self.policy_ref,
2241 }
2242 }
2243}
2244
2245impl crate::accounts::ContextDescriptor {
2246 pub const fn resolver_count(&self) -> usize {
2248 self.accounts.len()
2249 }
2250
2251 pub const fn effect_count(&self) -> usize {
2253 self.accounts.len() + if self.receipts_expected { 1 } else { 0 }
2254 }
2255
2256 pub fn find_resolver(&self, account: &str) -> Option<AccountResolverDescriptor> {
2258 let mut i = 0;
2259 while i < self.accounts.len() {
2260 if const_str_eq(self.accounts[i].name, account) {
2261 return Some(self.accounts[i].resolver_descriptor());
2262 }
2263 i += 1;
2264 }
2265 None
2266 }
2267
2268 pub fn find_effect(&self, target: &str) -> Option<InstructionEffectDescriptor> {
2270 let mut i = 0;
2271 while i < self.accounts.len() {
2272 if const_str_eq(self.accounts[i].name, target) {
2273 return Some(self.accounts[i].effect_descriptor());
2274 }
2275 i += 1;
2276 }
2277 None
2278 }
2279}
2280
2281#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2287pub enum ArgParseError {
2288 TooShort {
2290 required: u16,
2292 got: u16,
2294 },
2295}
2296
2297impl fmt::Display for ArgParseError {
2298 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2299 match self {
2300 ArgParseError::TooShort { required, got } => {
2301 write!(f, "args: too short (required {}, got {})", required, got)
2302 }
2303 }
2304 }
2305}
2306
2307#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2320pub struct ErrorDescriptor {
2321 pub name: &'static str,
2323 pub code: u32,
2325 pub invariant: &'static str,
2327 pub doc: &'static str,
2329}
2330
2331#[derive(Clone, Copy, Debug)]
2343pub struct ConstantDescriptor {
2344 pub name: &'static str,
2346 pub ty: &'static str,
2348 pub value: &'static str,
2350 pub docs: &'static str,
2352}
2353
2354#[derive(Clone, Copy, Debug)]
2360pub struct ErrorRegistry {
2361 pub enum_name: &'static str,
2363 pub errors: &'static [ErrorDescriptor],
2365}
2366
2367impl ErrorRegistry {
2368 pub fn find_by_code(&self, code: u32) -> Option<&ErrorDescriptor> {
2370 let mut i = 0;
2371 while i < self.errors.len() {
2372 if self.errors[i].code == code {
2373 return Some(&self.errors[i]);
2374 }
2375 i += 1;
2376 }
2377 None
2378 }
2379
2380 pub fn invariant_for(&self, code: u32) -> Option<&'static str> {
2382 self.find_by_code(code).and_then(|d| {
2383 if d.invariant.is_empty() {
2384 None
2385 } else {
2386 Some(d.invariant)
2387 }
2388 })
2389 }
2390}
2391
2392#[derive(Clone, Copy, Debug)]
2394pub struct InstructionDescriptor {
2395 pub name: &'static str,
2397 pub tag: u8,
2399 pub args: &'static [ArgDescriptor],
2401 pub accounts: &'static [AccountEntry],
2403 pub capabilities: &'static [&'static str],
2405 pub policy_pack: &'static str,
2407 pub receipt_expected: bool,
2409}
2410
2411#[derive(Clone, Copy)]
2413pub struct EventDescriptor {
2414 pub name: &'static str,
2416 pub tag: u8,
2418 pub fields: &'static [FieldDescriptor],
2420}
2421
2422#[derive(Clone, Copy)]
2424pub struct PolicyDescriptor {
2425 pub name: &'static str,
2427 pub capabilities: &'static [&'static str],
2429 pub requirements: &'static [&'static str],
2431 pub invariants: &'static [&'static str],
2433 pub receipt_profile: &'static str,
2435}
2436
2437#[derive(Clone, Copy)]
2442pub struct LayoutMetadata {
2443 pub name: &'static str,
2445 pub segment_roles: &'static [&'static str],
2447 pub append_safe: bool,
2449 pub migration_required: bool,
2451 pub rebuildable: bool,
2453 pub policy_pack: &'static str,
2455 pub invariant_pack: &'static [&'static str],
2457 pub receipt_profile: &'static str,
2459 pub phase_requirements: &'static [&'static str],
2461 pub trust_profile: &'static str,
2463 pub manager_hints: &'static [&'static str],
2465}
2466
2467#[derive(Clone, Copy)]
2469pub struct CompatibilityPair {
2470 pub from_layout: &'static str,
2472 pub from_version: u8,
2474 pub to_layout: &'static str,
2476 pub to_version: u8,
2478 pub policy: MigrationPolicy,
2480 pub backward_readable: bool,
2482}
2483
2484#[derive(Clone, Copy)]
2499pub struct ProgramManifest {
2500 pub name: &'static str,
2502 pub version: &'static str,
2504 pub description: &'static str,
2506 pub layouts: &'static [LayoutManifest],
2508 pub layout_metadata: &'static [LayoutMetadata],
2510 pub instructions: &'static [InstructionDescriptor],
2512 pub events: &'static [EventDescriptor],
2514 pub policies: &'static [PolicyDescriptor],
2516 pub compatibility_pairs: &'static [CompatibilityPair],
2518 pub tooling_hints: &'static [&'static str],
2520 pub contexts: &'static [crate::accounts::ContextDescriptor],
2522}
2523
2524#[derive(Clone, Copy)]
2530pub struct PdaSeedHint {
2531 pub kind: &'static str,
2533 pub value: &'static str,
2535}
2536
2537#[derive(Clone, Copy)]
2539pub struct IdlAccountEntry {
2540 pub name: &'static str,
2542 pub writable: bool,
2544 pub signer: bool,
2546 pub layout_ref: &'static str,
2548 pub pda_seeds: &'static [PdaSeedHint],
2550}
2551
2552#[derive(Clone, Copy)]
2554pub struct IdlInstructionDescriptor {
2555 pub name: &'static str,
2557 pub tag: u8,
2559 pub args: &'static [ArgDescriptor],
2561 pub accounts: &'static [IdlAccountEntry],
2563}
2564
2565#[derive(Clone, Copy)]
2573pub struct ProgramIdl {
2574 pub name: &'static str,
2576 pub version: &'static str,
2578 pub description: &'static str,
2580 pub instructions: &'static [IdlInstructionDescriptor],
2582 pub accounts: &'static [LayoutManifest],
2584 pub events: &'static [EventDescriptor],
2586 pub fingerprints: &'static [([u8; 8], &'static str)],
2588}
2589
2590impl ProgramIdl {
2591 pub const fn empty() -> Self {
2593 Self {
2594 name: "",
2595 version: "",
2596 description: "",
2597 instructions: &[],
2598 accounts: &[],
2599 events: &[],
2600 fingerprints: &[],
2601 }
2602 }
2603
2604 pub const fn instruction_count(&self) -> usize {
2606 self.instructions.len()
2607 }
2608
2609 pub const fn account_count(&self) -> usize {
2611 self.accounts.len()
2612 }
2613
2614 pub fn find_instruction(&self, name: &str) -> Option<&IdlInstructionDescriptor> {
2616 let mut i = 0;
2617 while i < self.instructions.len() {
2618 if const_str_eq(self.instructions[i].name, name) {
2619 return Some(&self.instructions[i]);
2620 }
2621 i += 1;
2622 }
2623 None
2624 }
2625
2626 pub fn find_account(&self, name: &str) -> Option<&LayoutManifest> {
2628 let mut i = 0;
2629 while i < self.accounts.len() {
2630 if const_str_eq(self.accounts[i].name, name) {
2631 return Some(&self.accounts[i]);
2632 }
2633 i += 1;
2634 }
2635 None
2636 }
2637}
2638
2639impl fmt::Display for ProgramIdl {
2640 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2641 writeln!(f, "IDL: {} {}", self.name, self.version)?;
2642 if !self.description.is_empty() {
2643 writeln!(f, " {}", self.description)?;
2644 }
2645 writeln!(f)?;
2646 writeln!(f, "Instructions ({}):", self.instructions.len())?;
2647 for ix in self.instructions.iter() {
2648 write!(
2649 f,
2650 " {:>2} {:16} args={} accounts={}",
2651 ix.tag,
2652 ix.name,
2653 ix.args.len(),
2654 ix.accounts.len()
2655 )?;
2656 writeln!(f)?;
2657 }
2658 writeln!(f)?;
2659 writeln!(f, "Accounts ({}):", self.accounts.len())?;
2660 for a in self.accounts.iter() {
2661 write!(
2662 f,
2663 " {:16} disc={} v{} {} bytes id=",
2664 a.name, a.disc, a.version, a.total_size
2665 )?;
2666 write_hex(f, &a.layout_id)?;
2667 writeln!(f)?;
2668 }
2669 if !self.events.is_empty() {
2670 writeln!(f)?;
2671 writeln!(f, "Events ({}):", self.events.len())?;
2672 for e in self.events.iter() {
2673 writeln!(f, " {:>2} {:16} fields={}", e.tag, e.name, e.fields.len())?;
2674 }
2675 }
2676 Ok(())
2677 }
2678}
2679
2680#[derive(Clone, Copy)]
2688pub struct CodamaInstruction {
2689 pub name: &'static str,
2690 pub discriminator: u8,
2691 pub args: &'static [ArgDescriptor],
2692 pub accounts: &'static [IdlAccountEntry],
2693}
2694
2695#[derive(Clone, Copy)]
2697pub struct CodamaAccount {
2698 pub name: &'static str,
2699 pub discriminator: u8,
2700 pub size: usize,
2701 pub fields: &'static [FieldDescriptor],
2702}
2703
2704#[derive(Clone, Copy)]
2706pub struct CodamaEvent {
2707 pub name: &'static str,
2708 pub discriminator: u8,
2709 pub fields: &'static [FieldDescriptor],
2710}
2711
2712#[derive(Clone, Copy)]
2728pub struct CodamaProjection {
2729 pub name: &'static str,
2731 pub version: &'static str,
2733 pub instructions: &'static [CodamaInstruction],
2735 pub accounts: &'static [CodamaAccount],
2737 pub events: &'static [CodamaEvent],
2739}
2740
2741impl CodamaProjection {
2742 pub const fn empty() -> Self {
2744 Self {
2745 name: "",
2746 version: "",
2747 instructions: &[],
2748 accounts: &[],
2749 events: &[],
2750 }
2751 }
2752}
2753
2754impl fmt::Display for CodamaProjection {
2755 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2756 writeln!(f, "Codama: {} {}", self.name, self.version)?;
2757 writeln!(f)?;
2758 writeln!(f, "Instructions ({}):", self.instructions.len())?;
2759 for ix in self.instructions.iter() {
2760 writeln!(
2761 f,
2762 " {:>2} {:16} args={} accounts={}",
2763 ix.discriminator,
2764 ix.name,
2765 ix.args.len(),
2766 ix.accounts.len()
2767 )?;
2768 }
2769 writeln!(f)?;
2770 writeln!(f, "Accounts ({}):", self.accounts.len())?;
2771 for a in self.accounts.iter() {
2772 writeln!(
2773 f,
2774 " {:16} disc={} {} bytes fields={}",
2775 a.name,
2776 a.discriminator,
2777 a.size,
2778 a.fields.len()
2779 )?;
2780 }
2781 if !self.events.is_empty() {
2782 writeln!(f)?;
2783 writeln!(f, "Events ({}):", self.events.len())?;
2784 for e in self.events.iter() {
2785 writeln!(
2786 f,
2787 " {:>2} {:16} fields={}",
2788 e.discriminator,
2789 e.name,
2790 e.fields.len()
2791 )?;
2792 }
2793 }
2794 Ok(())
2795 }
2796}
2797
2798impl ProgramManifest {
2799 pub const fn empty() -> Self {
2801 Self {
2802 name: "",
2803 version: "",
2804 description: "",
2805 layouts: &[],
2806 layout_metadata: &[],
2807 instructions: &[],
2808 events: &[],
2809 policies: &[],
2810 compatibility_pairs: &[],
2811 tooling_hints: &[],
2812 contexts: &[],
2813 }
2814 }
2815
2816 pub const fn layout_count(&self) -> usize {
2818 self.layouts.len()
2819 }
2820
2821 pub const fn instruction_count(&self) -> usize {
2823 self.instructions.len()
2824 }
2825
2826 pub fn find_layout_by_disc(&self, disc: u8) -> Option<&LayoutManifest> {
2828 let mut i = 0;
2829 while i < self.layouts.len() {
2830 if self.layouts[i].disc == disc {
2831 return Some(&self.layouts[i]);
2832 }
2833 i += 1;
2834 }
2835 None
2836 }
2837
2838 pub fn find_layout_by_id(&self, layout_id: &[u8; 8]) -> Option<&LayoutManifest> {
2840 let mut i = 0;
2841 while i < self.layouts.len() {
2842 if self.layouts[i].layout_id == *layout_id {
2843 return Some(&self.layouts[i]);
2844 }
2845 i += 1;
2846 }
2847 None
2848 }
2849
2850 pub fn identify_from_data(&self, data: &[u8]) -> Option<&LayoutManifest> {
2852 let header = decode_header(data)?;
2853 if let Some(m) = self.find_layout_by_id(&header.layout_id) {
2855 return Some(m);
2856 }
2857 self.find_layout_by_disc(header.disc)
2859 }
2860
2861 pub fn find_instruction(&self, tag: u8) -> Option<&InstructionDescriptor> {
2863 let mut i = 0;
2864 while i < self.instructions.len() {
2865 if self.instructions[i].tag == tag {
2866 return Some(&self.instructions[i]);
2867 }
2868 i += 1;
2869 }
2870 None
2871 }
2872
2873 pub fn find_policy(&self, name: &str) -> Option<&PolicyDescriptor> {
2875 let mut i = 0;
2876 while i < self.policies.len() {
2877 if self.policies[i].name == name {
2878 return Some(&self.policies[i]);
2879 }
2880 i += 1;
2881 }
2882 None
2883 }
2884
2885 pub fn find_layout_metadata(&self, name: &str) -> Option<&LayoutMetadata> {
2887 let mut i = 0;
2888 while i < self.layout_metadata.len() {
2889 if const_str_eq(self.layout_metadata[i].name, name) {
2890 return Some(&self.layout_metadata[i]);
2891 }
2892 i += 1;
2893 }
2894 None
2895 }
2896
2897 pub fn find_context(&self, name: &str) -> Option<&crate::accounts::ContextDescriptor> {
2899 let mut i = 0;
2900 while i < self.contexts.len() {
2901 if const_str_eq(self.contexts[i].name, name) {
2902 return Some(&self.contexts[i]);
2903 }
2904 i += 1;
2905 }
2906 None
2907 }
2908
2909 pub fn find_compat_pair(
2911 &self,
2912 from_name: &str,
2913 from_ver: u8,
2914 to_name: &str,
2915 to_ver: u8,
2916 ) -> Option<&CompatibilityPair> {
2917 let mut i = 0;
2918 while i < self.compatibility_pairs.len() {
2919 let cp = &self.compatibility_pairs[i];
2920 if const_str_eq(cp.from_layout, from_name)
2921 && cp.from_version == from_ver
2922 && const_str_eq(cp.to_layout, to_name)
2923 && cp.to_version == to_ver
2924 {
2925 return Some(cp);
2926 }
2927 i += 1;
2928 }
2929 None
2930 }
2931}
2932
2933impl fmt::Display for ProgramManifest {
2934 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2935 writeln!(f, "Program: {} {}", self.name, self.version)?;
2936 if !self.description.is_empty() {
2937 writeln!(f, " {}", self.description)?;
2938 }
2939 writeln!(f)?;
2940
2941 writeln!(f, "Layouts ({}):", self.layouts.len())?;
2942 for m in self.layouts.iter() {
2943 write!(
2944 f,
2945 " {:16} v{} disc={} {} bytes fingerprint=",
2946 m.name, m.version, m.disc, m.total_size
2947 )?;
2948 write_hex(f, &m.layout_id)?;
2949 if let Some(meta) = self.find_layout_metadata(m.name) {
2951 if !meta.trust_profile.is_empty() {
2952 write!(f, " trust={}", meta.trust_profile)?;
2953 }
2954 if meta.append_safe {
2955 write!(f, " append-safe")?;
2956 }
2957 if meta.migration_required {
2958 write!(f, " migration-required")?;
2959 }
2960 }
2961 writeln!(f)?;
2962 }
2963 writeln!(f)?;
2964
2965 writeln!(f, "Instructions ({}):", self.instructions.len())?;
2966 for ix in self.instructions.iter() {
2967 write!(
2968 f,
2969 " {:>2} {:16} accounts={}",
2970 ix.tag,
2971 ix.name,
2972 ix.accounts.len()
2973 )?;
2974 if !ix.capabilities.is_empty() {
2975 write!(f, " caps=")?;
2976 for (j, c) in ix.capabilities.iter().enumerate() {
2977 if j > 0 {
2978 write!(f, ",")?;
2979 }
2980 write!(f, "{}", c)?;
2981 }
2982 }
2983 if ix.receipt_expected {
2984 write!(f, " receipt=yes")?;
2985 }
2986 if let Some(ctx) = self.find_context(ix.name) {
2987 write!(
2988 f,
2989 " resolvers={} effects={}",
2990 ctx.resolver_count(),
2991 ctx.effect_count()
2992 )?;
2993 }
2994 writeln!(f)?;
2995 }
2996 writeln!(f)?;
2997
2998 if !self.policies.is_empty() {
2999 writeln!(f, "Policies ({}):", self.policies.len())?;
3000 for p in self.policies.iter() {
3001 write!(f, " {:24}", p.name)?;
3002 for (j, r) in p.requirements.iter().enumerate() {
3003 if j > 0 {
3004 write!(f, " + ")?;
3005 }
3006 write!(f, "{}", r)?;
3007 }
3008 if !p.receipt_profile.is_empty() {
3009 write!(f, " receipt={}", p.receipt_profile)?;
3010 }
3011 writeln!(f)?;
3012 }
3013 writeln!(f)?;
3014 }
3015
3016 if !self.events.is_empty() {
3017 writeln!(f, "Events ({}):", self.events.len())?;
3018 for e in self.events.iter() {
3019 writeln!(f, " {:>2} {:16} fields={}", e.tag, e.name, e.fields.len())?;
3020 }
3021 writeln!(f)?;
3022 }
3023
3024 if !self.compatibility_pairs.is_empty() {
3025 writeln!(f, "Compatibility ({}):", self.compatibility_pairs.len())?;
3026 for cp in self.compatibility_pairs.iter() {
3027 writeln!(
3028 f,
3029 " {} v{} -> {} v{} {}{}",
3030 cp.from_layout,
3031 cp.from_version,
3032 cp.to_layout,
3033 cp.to_version,
3034 cp.policy,
3035 if cp.backward_readable {
3036 " backward-readable"
3037 } else {
3038 ""
3039 },
3040 )?;
3041 }
3042 }
3043
3044 Ok(())
3045 }
3046}
3047
3048pub struct DecodedField<'a> {
3054 pub name: &'a str,
3056 pub canonical_type: &'a str,
3058 pub raw: &'a [u8],
3060 pub offset: u16,
3062 pub size: u16,
3064}
3065
3066impl<'a> DecodedField<'a> {
3067 pub fn format_value(&self, buf: &mut [u8]) -> usize {
3071 match self.canonical_type {
3072 "WireU64" | "LeU64" if self.raw.len() >= 8 => {
3073 let v = u64::from_le_bytes([
3074 self.raw[0],
3075 self.raw[1],
3076 self.raw[2],
3077 self.raw[3],
3078 self.raw[4],
3079 self.raw[5],
3080 self.raw[6],
3081 self.raw[7],
3082 ]);
3083 format_u64(v, buf)
3084 }
3085 "WireU32" | "LeU32" if self.raw.len() >= 4 => {
3086 let v =
3087 u32::from_le_bytes([self.raw[0], self.raw[1], self.raw[2], self.raw[3]]) as u64;
3088 format_u64(v, buf)
3089 }
3090 "WireU16" | "LeU16" if self.raw.len() >= 2 => {
3091 let v = u16::from_le_bytes([self.raw[0], self.raw[1]]) as u64;
3092 format_u64(v, buf)
3093 }
3094 "WireBool" | "LeBool" if !self.raw.is_empty() => {
3095 if self.raw[0] != 0 {
3096 let len = 4usize.min(buf.len());
3097 buf[..len].copy_from_slice(&b"true"[..len]);
3098 len
3099 } else {
3100 let len = 5usize.min(buf.len());
3101 buf[..len].copy_from_slice(&b"false"[..len]);
3102 len
3103 }
3104 }
3105 "u8" if self.raw.len() == 1 => format_u64(self.raw[0] as u64, buf),
3106 _ if self.size == 32 => {
3107 format_hex_truncated(self.raw, buf)
3109 }
3110 _ => format_hex_truncated(self.raw, buf),
3111 }
3112 }
3113}
3114
3115pub fn decode_account_fields<'a, const N: usize>(
3119 data: &'a [u8],
3120 manifest: &'a LayoutManifest,
3121) -> (usize, [Option<DecodedField<'a>>; N]) {
3122 let mut fields: [Option<DecodedField<'a>>; N] = [const { None }; N];
3123 let count = manifest.field_count.min(N);
3124 let mut i = 0;
3125 while i < count {
3126 let fd = &manifest.fields[i];
3127 let start = fd.offset as usize;
3128 let end = start + fd.size as usize;
3129 if end <= data.len() {
3130 fields[i] = Some(DecodedField {
3131 name: fd.name,
3132 canonical_type: fd.canonical_type,
3133 raw: &data[start..end],
3134 offset: fd.offset,
3135 size: fd.size,
3136 });
3137 }
3138 i += 1;
3139 }
3140 (count, fields)
3141}
3142
3143fn format_u64(mut v: u64, buf: &mut [u8]) -> usize {
3145 if v == 0 {
3146 if !buf.is_empty() {
3147 buf[0] = b'0';
3148 return 1;
3149 }
3150 return 0;
3151 }
3152 let mut tmp = [0u8; 20];
3154 let mut pos = 0;
3155 while v > 0 && pos < 20 {
3156 tmp[pos] = b'0' + (v % 10) as u8;
3157 v /= 10;
3158 pos += 1;
3159 }
3160 let len = pos.min(buf.len());
3161 let mut i = 0;
3162 while i < len {
3163 buf[i] = tmp[pos - 1 - i];
3164 i += 1;
3165 }
3166 len
3167}
3168
3169fn format_hex_truncated(bytes: &[u8], buf: &mut [u8]) -> usize {
3171 const HEX: &[u8; 16] = b"0123456789abcdef";
3172 let max_bytes = if bytes.len() > 8 { 8 } else { bytes.len() };
3173 let mut pos = 0;
3174 if buf.len() >= 2 {
3176 buf[0] = b'0';
3177 buf[1] = b'x';
3178 pos = 2;
3179 }
3180 let mut i = 0;
3181 while i < max_bytes && pos + 1 < buf.len() {
3182 buf[pos] = HEX[(bytes[i] >> 4) as usize];
3183 buf[pos + 1] = HEX[(bytes[i] & 0xf) as usize];
3184 pos += 2;
3185 i += 1;
3186 }
3187 if bytes.len() > 8 && pos + 3 <= buf.len() {
3188 buf[pos] = b'.';
3189 buf[pos + 1] = b'.';
3190 buf[pos + 2] = b'.';
3191 pos += 3;
3192 }
3193 pos
3194}
3195
3196#[repr(C)]
3219#[derive(Clone, Copy)]
3220pub struct HopperSchemaPointer {
3221 pub schema_version: u16,
3223 pub pointer_flags: u16,
3225 pub manifest_hash: [u8; 32],
3227 pub idl_hash: [u8; 32],
3229 pub codama_hash: [u8; 32],
3231 pub uri_len: u16,
3233 pub uri: [u8; 192],
3235}
3236
3237impl HopperSchemaPointer {
3238 pub const DISC: u8 = 255;
3240
3241 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";
3249
3250 pub const FLAG_HAS_MANIFEST: u16 = 0x0001;
3252 pub const FLAG_HAS_IDL: u16 = 0x0002;
3253 pub const FLAG_HAS_CODAMA: u16 = 0x0004;
3254 pub const FLAG_HAS_URI: u16 = 0x0008;
3255 pub const FLAG_URI_IS_IPFS: u16 = 0x0010;
3256 pub const FLAG_URI_IS_ARWEAVE: u16 = 0x0020;
3257
3258 pub fn uri_str(&self) -> &str {
3260 let len = (self.uri_len as usize).min(192);
3261 core::str::from_utf8(&self.uri[..len]).unwrap_or("")
3263 }
3264
3265 #[inline(always)]
3267 pub fn has_flag(&self, flag: u16) -> bool {
3268 self.pointer_flags & flag != 0
3269 }
3270}
3271
3272#[derive(Clone, Copy, Debug)]
3279pub struct SemanticLint {
3280 pub severity: LintSeverity,
3282 pub code: &'static str,
3284 pub message: &'static str,
3286 pub field: &'static str,
3288}
3289
3290#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3292#[repr(u8)]
3293pub enum LintSeverity {
3294 Info = 0,
3296 Warning = 1,
3298 Error = 2,
3300}
3301
3302impl LintSeverity {
3303 pub const fn name(self) -> &'static str {
3305 match self {
3306 Self::Info => "info",
3307 Self::Warning => "warning",
3308 Self::Error => "error",
3309 }
3310 }
3311}
3312
3313pub fn lint_layout<const N: usize>(
3317 manifest: &LayoutManifest,
3318 behavior: &LayoutBehavior,
3319) -> (usize, [SemanticLint; N]) {
3320 let mut lints = [SemanticLint {
3321 severity: LintSeverity::Info,
3322 code: "",
3323 message: "",
3324 field: "",
3325 }; N];
3326 let mut count = 0usize;
3327
3328 let mut i = 0;
3329 while i < manifest.field_count {
3330 let field = &manifest.fields[i];
3331
3332 if field.intent.is_authority_sensitive()
3334 && behavior.mutation_class.is_mutating()
3335 && !behavior.requires_signer
3336 {
3337 if count < N {
3338 lints[count] = SemanticLint {
3339 severity: LintSeverity::Error,
3340 code: "E001",
3341 message:
3342 "Authority-sensitive field in mutable layout without signer requirement",
3343 field: field.name,
3344 };
3345 count += 1;
3346 }
3347 }
3348
3349 if field.intent.is_monetary()
3351 && behavior.mutation_class.is_mutating()
3352 && !matches!(behavior.mutation_class, MutationClass::Financial)
3353 {
3354 if count < N {
3355 lints[count] = SemanticLint {
3356 severity: LintSeverity::Warning,
3357 code: "W001",
3358 message: "Monetary field in layout without financial mutation class",
3359 field: field.name,
3360 };
3361 count += 1;
3362 }
3363 }
3364
3365 if field.intent.is_init_only()
3367 && behavior.mutation_class.is_mutating()
3368 && !matches!(behavior.mutation_class, MutationClass::AppendOnly)
3369 {
3370 if count < N {
3371 lints[count] = SemanticLint {
3372 severity: LintSeverity::Warning,
3373 code: "W002",
3374 message: "Init-only field (PDA seed or bump) in mutable layout. Consider making read-only or append-only.",
3375 field: field.name,
3376 };
3377 count += 1;
3378 }
3379 }
3380
3381 i += 1;
3382 }
3383
3384 if behavior.mutation_class.is_mutating() && !behavior.requires_signer {
3388 if count < N {
3389 lints[count] = SemanticLint {
3390 severity: LintSeverity::Warning,
3391 code: "W003",
3392 message: "Mutable layout does not require signer. Verify this is intentional.",
3393 field: "",
3394 };
3395 count += 1;
3396 }
3397 }
3398
3399 if behavior.affects_balance {
3401 let mut has_balance = false;
3402 let mut j = 0;
3403 while j < manifest.field_count {
3404 if manifest.fields[j].intent.is_monetary() {
3405 has_balance = true;
3406 }
3407 j += 1;
3408 }
3409 if !has_balance && count < N {
3410 lints[count] = SemanticLint {
3411 severity: LintSeverity::Warning,
3412 code: "W004",
3413 message: "Layout behavior declares affects_balance but no monetary fields found",
3414 field: "",
3415 };
3416 count += 1;
3417 }
3418 }
3419
3420 (count, lints)
3421}
3422
3423#[cfg(feature = "policy")]
3429pub fn lint_policy<const N: usize>(
3430 behavior: &LayoutBehavior,
3431 policy: hopper_core::policy::PolicyClass,
3432) -> (usize, [SemanticLint; N]) {
3433 let mut lints = [SemanticLint {
3434 severity: LintSeverity::Info,
3435 code: "",
3436 message: "",
3437 field: "",
3438 }; N];
3439 let mut count = 0usize;
3440
3441 if matches!(behavior.mutation_class, MutationClass::Financial)
3443 && !matches!(policy, hopper_core::policy::PolicyClass::Financial)
3444 {
3445 if count < N {
3446 lints[count] = SemanticLint {
3447 severity: LintSeverity::Warning,
3448 code: "W005",
3449 message: "Financial mutation class but policy class is not Financial",
3450 field: "",
3451 };
3452 count += 1;
3453 }
3454 }
3455
3456 if matches!(policy, hopper_core::policy::PolicyClass::Financial)
3458 && !matches!(behavior.mutation_class, MutationClass::Financial)
3459 {
3460 if count < N {
3461 lints[count] = SemanticLint {
3462 severity: LintSeverity::Warning,
3463 code: "W006",
3464 message: "Financial policy class but mutation class is not Financial",
3465 field: "",
3466 };
3467 count += 1;
3468 }
3469 }
3470
3471 (count, lints)
3472}
3473
3474impl fmt::Display for SemanticLint {
3475 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3476 write!(
3477 f,
3478 "[{}] {}: {}",
3479 self.severity.name(),
3480 self.code,
3481 self.message
3482 )?;
3483 if !self.field.is_empty() {
3484 write!(f, " (field: {})", self.field)?;
3485 }
3486 Ok(())
3487 }
3488}
3489
3490pub struct OperatingProfile {
3499 pub financial_fields: [&'static str; 16],
3501 pub financial_count: u8,
3503 pub authority_surfaces: [&'static str; 16],
3505 pub authority_count: u8,
3507 pub append_only_segments: [&'static str; 8],
3509 pub append_only_count: u8,
3511 pub migration_sensitive: [&'static str; 8],
3513 pub migration_sensitive_count: u8,
3515 pub stability_grades: [(&'static str, LayoutStabilityGrade); 8],
3517 pub stability_count: u8,
3519 pub has_financial_ops: bool,
3521 pub has_cpi_ops: bool,
3523 pub has_migration_paths: bool,
3525 pub has_receipts: bool,
3527}
3528
3529impl OperatingProfile {
3530 pub fn from_manifest(manifest: &ProgramManifest) -> Self {
3532 let mut profile = Self {
3533 financial_fields: [""; 16],
3534 financial_count: 0,
3535 authority_surfaces: [""; 16],
3536 authority_count: 0,
3537 append_only_segments: [""; 8],
3538 append_only_count: 0,
3539 migration_sensitive: [""; 8],
3540 migration_sensitive_count: 0,
3541 stability_grades: [("", LayoutStabilityGrade::Stable); 8],
3542 stability_count: 0,
3543 has_financial_ops: false,
3544 has_cpi_ops: false,
3545 has_migration_paths: !manifest.compatibility_pairs.is_empty(),
3546 has_receipts: false,
3547 };
3548
3549 let mut li = 0;
3551 while li < manifest.layouts.len() {
3552 let layout = &manifest.layouts[li];
3553
3554 if (profile.stability_count as usize) < 8 {
3556 profile.stability_grades[profile.stability_count as usize] =
3557 (layout.name, LayoutStabilityGrade::compute(layout));
3558 profile.stability_count += 1;
3559 }
3560
3561 let mut fi = 0;
3562 while fi < layout.field_count {
3563 let field = &layout.fields[fi];
3564 if field.intent.is_monetary() && (profile.financial_count as usize) < 16 {
3565 profile.financial_fields[profile.financial_count as usize] = field.name;
3566 profile.financial_count += 1;
3567 }
3568 if field.intent.is_authority_sensitive() && (profile.authority_count as usize) < 16
3569 {
3570 profile.authority_surfaces[profile.authority_count as usize] = field.name;
3571 profile.authority_count += 1;
3572 }
3573 fi += 1;
3574 }
3575 li += 1;
3576 }
3577
3578 let mut mi = 0;
3580 while mi < manifest.layout_metadata.len() {
3581 let meta = &manifest.layout_metadata[mi];
3582 let mut si = 0;
3583 while si < meta.segment_roles.len() {
3584 let role_name = meta.segment_roles[si];
3585 if (const_str_eq(role_name, "Journal") || const_str_eq(role_name, "Audit"))
3586 && (profile.append_only_count as usize) < 8
3587 {
3588 profile.append_only_segments[profile.append_only_count as usize] = role_name;
3589 profile.append_only_count += 1;
3590 }
3591 if const_str_eq(role_name, "Core")
3592 && (profile.migration_sensitive_count as usize) < 8
3593 {
3594 profile.migration_sensitive[profile.migration_sensitive_count as usize] =
3595 meta.name;
3596 profile.migration_sensitive_count += 1;
3597 }
3598 si += 1;
3599 }
3600 mi += 1;
3601 }
3602
3603 let mut ii = 0;
3605 while ii < manifest.instructions.len() {
3606 let ix = &manifest.instructions[ii];
3607 if ix.receipt_expected {
3608 profile.has_receipts = true;
3609 }
3610 let mut ci = 0;
3611 while ci < ix.capabilities.len() {
3612 if const_str_eq(ix.capabilities[ci], "MutatesTreasury") {
3613 profile.has_financial_ops = true;
3614 }
3615 if const_str_eq(ix.capabilities[ci], "ExternalCall") {
3616 profile.has_cpi_ops = true;
3617 }
3618 ci += 1;
3619 }
3620 ii += 1;
3621 }
3622
3623 profile
3624 }
3625}
3626
3627impl fmt::Display for OperatingProfile {
3628 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3629 writeln!(f, "Operating Profile:")?;
3630
3631 if self.financial_count > 0 {
3632 write!(f, " Financial fields:")?;
3633 let mut i = 0;
3634 while i < self.financial_count as usize {
3635 write!(f, " {}", self.financial_fields[i])?;
3636 i += 1;
3637 }
3638 writeln!(f)?;
3639 }
3640
3641 if self.authority_count > 0 {
3642 write!(f, " Authority surfaces:")?;
3643 let mut i = 0;
3644 while i < self.authority_count as usize {
3645 write!(f, " {}", self.authority_surfaces[i])?;
3646 i += 1;
3647 }
3648 writeln!(f)?;
3649 }
3650
3651 if self.append_only_count > 0 {
3652 write!(f, " Append-only segments:")?;
3653 let mut i = 0;
3654 while i < self.append_only_count as usize {
3655 write!(f, " {}", self.append_only_segments[i])?;
3656 i += 1;
3657 }
3658 writeln!(f)?;
3659 }
3660
3661 if self.stability_count > 0 {
3662 writeln!(f, " Stability grades:")?;
3663 let mut i = 0;
3664 while i < self.stability_count as usize {
3665 let (name, grade) = self.stability_grades[i];
3666 writeln!(f, " {}: {}", name, grade.name())?;
3667 i += 1;
3668 }
3669 }
3670
3671 write!(f, " Features:")?;
3672 if self.has_financial_ops {
3673 write!(f, " financial")?;
3674 }
3675 if self.has_cpi_ops {
3676 write!(f, " cpi")?;
3677 }
3678 if self.has_migration_paths {
3679 write!(f, " migration")?;
3680 }
3681 if self.has_receipts {
3682 write!(f, " receipts")?;
3683 }
3684 writeln!(f)?;
3685
3686 Ok(())
3687 }
3688}
3689
3690pub struct HopperIdl {
3700 pub base: ProgramIdl,
3702 pub policies: &'static [PolicyDescriptor],
3704 pub compatibility: &'static [CompatibilityPair],
3706 pub receipt_profiles: &'static [ReceiptProfile],
3708 pub segment_metadata: &'static [IdlSegmentDescriptor],
3710 pub contexts: &'static [crate::accounts::ContextDescriptor],
3712}
3713
3714#[derive(Clone, Copy)]
3716pub struct ReceiptProfile {
3717 pub name: &'static str,
3719 pub expected_phase: &'static str,
3721 pub expects_balance_change: bool,
3723 pub expects_authority_change: bool,
3725 pub expects_journal_append: bool,
3727 pub min_changed_fields: u8,
3729}
3730
3731impl fmt::Display for ReceiptProfile {
3732 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3733 write!(f, "{}(phase={}", self.name, self.expected_phase)?;
3734 if self.expects_balance_change {
3735 write!(f, " balance")?;
3736 }
3737 if self.expects_authority_change {
3738 write!(f, " authority")?;
3739 }
3740 if self.expects_journal_append {
3741 write!(f, " journal")?;
3742 }
3743 if self.min_changed_fields > 0 {
3744 write!(f, " min_fields={}", self.min_changed_fields)?;
3745 }
3746 write!(f, ")")
3747 }
3748}
3749
3750#[derive(Clone, Copy)]
3752pub struct IdlSegmentDescriptor {
3753 pub name: &'static str,
3755 pub role: &'static str,
3757 pub append_only: bool,
3759 pub rebuildable: bool,
3761 pub must_preserve: bool,
3763}
3764
3765impl fmt::Display for IdlSegmentDescriptor {
3766 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3767 write!(f, "{}(role={}", self.name, self.role)?;
3768 if self.append_only {
3769 write!(f, " append-only")?;
3770 }
3771 if self.rebuildable {
3772 write!(f, " rebuildable")?;
3773 }
3774 if self.must_preserve {
3775 write!(f, " must-preserve")?;
3776 }
3777 write!(f, ")")
3778 }
3779}
3780
3781impl HopperIdl {
3782 pub const fn empty() -> Self {
3784 Self {
3785 base: ProgramIdl::empty(),
3786 policies: &[],
3787 compatibility: &[],
3788 receipt_profiles: &[],
3789 segment_metadata: &[],
3790 contexts: &[],
3791 }
3792 }
3793}
3794
3795impl fmt::Display for HopperIdl {
3796 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3797 write!(f, "{}", self.base)?;
3798
3799 if !self.policies.is_empty() {
3800 writeln!(f)?;
3801 writeln!(f, "Policies ({}):", self.policies.len())?;
3802 for p in self.policies.iter() {
3803 write!(f, " {:24}", p.name)?;
3804 for (j, r) in p.requirements.iter().enumerate() {
3805 if j > 0 {
3806 write!(f, " + ")?;
3807 }
3808 write!(f, "{}", r)?;
3809 }
3810 writeln!(f)?;
3811 }
3812 }
3813
3814 if !self.compatibility.is_empty() {
3815 writeln!(f)?;
3816 writeln!(f, "Compatibility ({}):", self.compatibility.len())?;
3817 for cp in self.compatibility.iter() {
3818 writeln!(
3819 f,
3820 " {} v{} -> {} v{} {}",
3821 cp.from_layout, cp.from_version, cp.to_layout, cp.to_version, cp.policy,
3822 )?;
3823 }
3824 }
3825
3826 if !self.receipt_profiles.is_empty() {
3827 writeln!(f)?;
3828 writeln!(f, "Receipt Profiles ({}):", self.receipt_profiles.len())?;
3829 for rp in self.receipt_profiles.iter() {
3830 writeln!(
3831 f,
3832 " {:24} phase={} balance={} authority={} journal={}",
3833 rp.name,
3834 rp.expected_phase,
3835 rp.expects_balance_change,
3836 rp.expects_authority_change,
3837 rp.expects_journal_append,
3838 )?;
3839 }
3840 }
3841
3842 if !self.segment_metadata.is_empty() {
3843 writeln!(f)?;
3844 writeln!(f, "Segments ({}):", self.segment_metadata.len())?;
3845 for s in self.segment_metadata.iter() {
3846 write!(f, " {:16} role={}", s.name, s.role)?;
3847 if s.append_only {
3848 write!(f, " append-only")?;
3849 }
3850 if s.rebuildable {
3851 write!(f, " rebuildable")?;
3852 }
3853 if s.must_preserve {
3854 write!(f, " must-preserve")?;
3855 }
3856 writeln!(f)?;
3857 }
3858 }
3859
3860 if !self.contexts.is_empty() {
3861 writeln!(f)?;
3862 writeln!(f, "Contexts ({}):", self.contexts.len())?;
3863 for ctx in self.contexts.iter() {
3864 write!(f, " {}", ctx)?;
3865 }
3866 }
3867
3868 Ok(())
3869 }
3870}
3871
3872#[derive(Clone, Copy, Debug)]
3878pub struct ManagerMetadata {
3879 pub layout: LayoutInfo,
3881 pub fields: &'static [FieldInfo],
3883}
3884
3885#[derive(Clone, Copy, Debug)]
3887pub struct SchemaBundle {
3888 pub manager: ManagerMetadata,
3889 pub manifest: LayoutManifest,
3890}
3891
3892pub trait SchemaExport: LayoutContract {
3906 #[inline(always)]
3908 fn layout_info() -> LayoutInfo {
3909 <Self as LayoutContract>::layout_info_static()
3910 }
3911
3912 #[inline(always)]
3914 fn field_map() -> &'static [FieldInfo] {
3915 <Self as LayoutContract>::fields()
3916 }
3917
3918 #[inline(always)]
3920 fn manager_metadata() -> ManagerMetadata {
3921 ManagerMetadata {
3922 layout: Self::layout_info(),
3923 fields: Self::field_map(),
3924 }
3925 }
3926
3927 #[inline(always)]
3929 fn schema_bundle() -> SchemaBundle {
3930 SchemaBundle {
3931 manager: Self::manager_metadata(),
3932 manifest: Self::layout_manifest(),
3933 }
3934 }
3935
3936 fn layout_manifest() -> LayoutManifest;
3938}
3939
3940pub trait AccountSchemaExt {
3942 fn manager_metadata_for<T: SchemaExport>(&self) -> Option<ManagerMetadata>;
3944
3945 fn schema_bundle_for<T: SchemaExport>(&self) -> Option<SchemaBundle>;
3947}
3948
3949impl AccountSchemaExt for AccountView {
3950 #[inline]
3951 fn manager_metadata_for<T: SchemaExport>(&self) -> Option<ManagerMetadata> {
3952 let info = self.layout_info()?;
3953 if info.matches::<T>() {
3954 Some(T::manager_metadata())
3955 } else {
3956 None
3957 }
3958 }
3959
3960 #[inline]
3961 fn schema_bundle_for<T: SchemaExport>(&self) -> Option<SchemaBundle> {
3962 let info = self.layout_info()?;
3963 if info.matches::<T>() {
3964 Some(T::schema_bundle())
3965 } else {
3966 None
3967 }
3968 }
3969}
3970
3971#[cfg(test)]
3974mod tests {
3975 use super::*;
3976
3977 const V1_FIELDS: &[FieldDescriptor] = &[
3978 FieldDescriptor {
3979 name: "authority",
3980 canonical_type: "[u8;32]",
3981 size: 32,
3982 offset: 16,
3983 intent: FieldIntent::Custom,
3984 },
3985 FieldDescriptor {
3986 name: "balance",
3987 canonical_type: "WireU64",
3988 size: 8,
3989 offset: 48,
3990 intent: FieldIntent::Custom,
3991 },
3992 ];
3993
3994 const V2_FIELDS: &[FieldDescriptor] = &[
3995 FieldDescriptor {
3996 name: "authority",
3997 canonical_type: "[u8;32]",
3998 size: 32,
3999 offset: 16,
4000 intent: FieldIntent::Custom,
4001 },
4002 FieldDescriptor {
4003 name: "balance",
4004 canonical_type: "WireU64",
4005 size: 8,
4006 offset: 48,
4007 intent: FieldIntent::Custom,
4008 },
4009 FieldDescriptor {
4010 name: "bump",
4011 canonical_type: "u8",
4012 size: 1,
4013 offset: 56,
4014 intent: FieldIntent::Custom,
4015 },
4016 ];
4017
4018 const V1_MANIFEST: LayoutManifest = LayoutManifest {
4019 name: "Vault",
4020 disc: 1,
4021 version: 1,
4022 layout_id: [1, 2, 3, 4, 5, 6, 7, 8],
4023 total_size: 56,
4024 field_count: 2,
4025 fields: V1_FIELDS,
4026 };
4027
4028 const V2_MANIFEST: LayoutManifest = LayoutManifest {
4029 name: "Vault",
4030 disc: 1,
4031 version: 2,
4032 layout_id: [10, 20, 30, 40, 50, 60, 70, 80],
4033 total_size: 57,
4034 field_count: 3,
4035 fields: V2_FIELDS,
4036 };
4037
4038 #[test]
4039 fn no_op_for_identical() {
4040 let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &V1_MANIFEST);
4041 assert_eq!(plan.policy, MigrationPolicy::NoOp);
4042 assert_eq!(plan.step_count, 0);
4043 }
4044
4045 #[test]
4046 fn append_only_migration() {
4047 let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &V2_MANIFEST);
4048 assert_eq!(plan.policy, MigrationPolicy::AppendOnly);
4049 assert!(plan.step_count >= 3); assert_eq!(plan.old_size, 56);
4051 assert_eq!(plan.new_size, 57);
4052 assert!(plan.copy_bytes > 0);
4053 assert!(plan.zero_bytes > 0);
4054
4055 assert_eq!(plan.steps[0].action, MigrationAction::CopyPrefix);
4057 let mut found_zero = false;
4059 let mut i = 0;
4060 while i < plan.step_count {
4061 if plan.steps[i].action == MigrationAction::ZeroInit {
4062 assert_eq!(plan.steps[i].field, "bump");
4063 assert_eq!(plan.steps[i].size, 1);
4064 found_zero = true;
4065 }
4066 i += 1;
4067 }
4068 assert!(found_zero);
4069 }
4070
4071 #[test]
4072 fn incompatible_different_disc() {
4073 let other = LayoutManifest {
4074 disc: 99,
4075 ..V2_MANIFEST
4076 };
4077 let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &other);
4078 assert_eq!(plan.policy, MigrationPolicy::Incompatible);
4079 }
4080
4081 #[test]
4082 fn breaking_change_detected() {
4083 let changed_fields: &[FieldDescriptor] = &[
4084 FieldDescriptor {
4085 name: "authority",
4086 canonical_type: "WireU64",
4087 size: 8,
4088 offset: 16,
4089 intent: FieldIntent::Custom,
4090 },
4091 FieldDescriptor {
4092 name: "balance",
4093 canonical_type: "WireU64",
4094 size: 8,
4095 offset: 24,
4096 intent: FieldIntent::Custom,
4097 },
4098 ];
4099 let breaking = LayoutManifest {
4100 name: "Vault",
4101 disc: 1,
4102 version: 2,
4103 layout_id: [99; 8],
4104 total_size: 32,
4105 field_count: 2,
4106 fields: changed_fields,
4107 };
4108 let plan = MigrationPlan::<16>::generate(&V1_MANIFEST, &breaking);
4109 assert_eq!(plan.policy, MigrationPolicy::RequiresMigration);
4110 }
4111
4112 #[test]
4117 fn verdict_identical() {
4118 let v = CompatibilityVerdict::between(&V1_MANIFEST, &V1_MANIFEST);
4119 assert_eq!(v, CompatibilityVerdict::Identical);
4120 assert!(v.is_safe());
4121 assert!(v.is_backward_readable());
4122 assert!(!v.requires_migration());
4123 }
4124
4125 #[test]
4126 fn verdict_append_safe() {
4127 let v = CompatibilityVerdict::between(&V1_MANIFEST, &V2_MANIFEST);
4128 assert_eq!(v, CompatibilityVerdict::AppendSafe);
4129 assert!(v.is_safe());
4130 assert!(v.is_backward_readable());
4131 assert!(!v.requires_migration());
4132 }
4133
4134 #[test]
4135 fn verdict_migration_required() {
4136 let changed_fields: &[FieldDescriptor] = &[
4137 FieldDescriptor {
4138 name: "authority",
4139 canonical_type: "WireU64",
4140 size: 8,
4141 offset: 16,
4142 intent: FieldIntent::Custom,
4143 },
4144 FieldDescriptor {
4145 name: "balance",
4146 canonical_type: "WireU64",
4147 size: 8,
4148 offset: 24,
4149 intent: FieldIntent::Custom,
4150 },
4151 ];
4152 let breaking = LayoutManifest {
4153 name: "Vault",
4154 disc: 1,
4155 version: 2,
4156 layout_id: [99; 8],
4157 total_size: 32,
4158 field_count: 2,
4159 fields: changed_fields,
4160 };
4161 let v = CompatibilityVerdict::between(&V1_MANIFEST, &breaking);
4162 assert_eq!(v, CompatibilityVerdict::MigrationRequired);
4163 assert!(!v.is_safe());
4164 assert!(!v.is_backward_readable());
4165 assert!(v.requires_migration());
4166 }
4167
4168 #[test]
4169 fn verdict_wire_compatible() {
4170 let semantic_variant = LayoutManifest {
4172 layout_id: [77; 8], ..V1_MANIFEST };
4175 let v = CompatibilityVerdict::between(&V1_MANIFEST, &semantic_variant);
4176 assert_eq!(v, CompatibilityVerdict::WireCompatible);
4177 assert!(v.is_safe());
4178 assert!(v.is_backward_readable());
4179 assert!(!v.requires_migration());
4180 }
4181
4182 #[test]
4183 fn verdict_incompatible() {
4184 let other = LayoutManifest {
4185 disc: 99,
4186 ..V2_MANIFEST
4187 };
4188 let v = CompatibilityVerdict::between(&V1_MANIFEST, &other);
4189 assert_eq!(v, CompatibilityVerdict::Incompatible);
4190 assert!(!v.is_safe());
4191 }
4192
4193 #[test]
4194 fn verdict_names() {
4195 assert_eq!(CompatibilityVerdict::Identical.name(), "identical");
4196 assert_eq!(
4197 CompatibilityVerdict::WireCompatible.name(),
4198 "wire-compatible"
4199 );
4200 assert_eq!(CompatibilityVerdict::AppendSafe.name(), "append-safe");
4201 assert_eq!(
4202 CompatibilityVerdict::MigrationRequired.name(),
4203 "migration-required"
4204 );
4205 assert_eq!(CompatibilityVerdict::Incompatible.name(), "incompatible");
4206 }
4207
4208 #[test]
4209 fn segment_advice_core_must_preserve() {
4210 let segs = [DecodedSegment {
4211 id: [1, 0, 0, 0],
4212 offset: 36,
4213 size: 100,
4214 flags: 0x0000, version: 1,
4216 }];
4217 let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
4218 assert_eq!(report.count, 1);
4219 assert_eq!(report.advice[0].role, SegmentRoleHint::Core);
4220 assert!(report.advice[0].must_preserve);
4221 assert!(!report.advice[0].clearable);
4222 assert_eq!(report.preserve_bytes, 100);
4223 }
4224
4225 #[test]
4226 fn segment_advice_journal_clearable() {
4227 let segs = [DecodedSegment {
4228 id: [2, 0, 0, 0],
4229 offset: 136,
4230 size: 256,
4231 flags: 0x2000, version: 1,
4233 }];
4234 let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
4235 assert_eq!(report.advice[0].role, SegmentRoleHint::Journal);
4236 assert!(report.advice[0].clearable);
4237 assert!(report.advice[0].append_only);
4238 assert!(!report.advice[0].must_preserve);
4239 assert_eq!(report.clearable_bytes, 256);
4240 }
4241
4242 #[test]
4243 fn segment_advice_cache_rebuildable() {
4244 let segs = [DecodedSegment {
4245 id: [3, 0, 0, 0],
4246 offset: 400,
4247 size: 64,
4248 flags: 0x4000, version: 1,
4250 }];
4251 let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
4252 assert_eq!(report.advice[0].role, SegmentRoleHint::Cache);
4253 assert!(report.advice[0].clearable);
4254 assert!(report.advice[0].rebuildable);
4255 }
4256
4257 #[test]
4258 fn segment_advice_audit_immutable() {
4259 let segs = [DecodedSegment {
4260 id: [4, 0, 0, 0],
4261 offset: 200,
4262 size: 32,
4263 flags: 0x5000, version: 1,
4265 }];
4266 let report = SegmentMigrationReport::<4>::analyze(&segs, 1);
4267 assert_eq!(report.advice[0].role, SegmentRoleHint::Audit);
4268 assert!(report.advice[0].must_preserve);
4269 assert!(report.advice[0].immutable);
4270 assert!(report.advice[0].append_only);
4271 assert!(!report.advice[0].clearable);
4272 }
4273
4274 #[test]
4275 fn segment_advice_mixed_report() {
4276 let segs = [
4277 DecodedSegment {
4278 id: [1, 0, 0, 0],
4279 offset: 36,
4280 size: 100,
4281 flags: 0x0000,
4282 version: 1,
4283 },
4284 DecodedSegment {
4285 id: [2, 0, 0, 0],
4286 offset: 136,
4287 size: 200,
4288 flags: 0x2000,
4289 version: 1,
4290 },
4291 DecodedSegment {
4292 id: [3, 0, 0, 0],
4293 offset: 336,
4294 size: 64,
4295 flags: 0x4000,
4296 version: 1,
4297 },
4298 ];
4299 let report = SegmentMigrationReport::<8>::analyze(&segs, 3);
4300 assert_eq!(report.count, 3);
4301 assert_eq!(report.must_preserve_count(), 1);
4302 assert_eq!(report.clearable_count(), 2);
4303 assert_eq!(report.preserve_bytes, 100);
4304 assert_eq!(report.clearable_bytes, 264);
4305 assert_eq!(report.rebuildable_bytes, 64);
4306 }
4307
4308 #[test]
4309 fn segment_role_hint_requires_migration_copy() {
4310 assert!(SegmentRoleHint::Core.requires_migration_copy());
4311 assert!(SegmentRoleHint::Audit.requires_migration_copy());
4312 assert!(!SegmentRoleHint::Extension.requires_migration_copy());
4313 assert!(!SegmentRoleHint::Journal.requires_migration_copy());
4314 assert!(!SegmentRoleHint::Index.requires_migration_copy());
4315 assert!(!SegmentRoleHint::Cache.requires_migration_copy());
4316 assert!(!SegmentRoleHint::Shard.requires_migration_copy());
4317 }
4318
4319 #[test]
4320 fn segment_role_hint_is_safe_to_drop() {
4321 assert!(SegmentRoleHint::Cache.is_safe_to_drop());
4322 assert!(!SegmentRoleHint::Core.is_safe_to_drop());
4323 assert!(!SegmentRoleHint::Extension.is_safe_to_drop());
4324 assert!(!SegmentRoleHint::Journal.is_safe_to_drop());
4325 assert!(!SegmentRoleHint::Index.is_safe_to_drop());
4326 assert!(!SegmentRoleHint::Audit.is_safe_to_drop());
4327 assert!(!SegmentRoleHint::Shard.is_safe_to_drop());
4328 }
4329
4330 static PM_LAYOUTS: &[LayoutManifest] = &[
4335 LayoutManifest {
4336 name: "Vault",
4337 disc: 1,
4338 version: 1,
4339 layout_id: [1, 2, 3, 4, 5, 6, 7, 8],
4340 total_size: 57,
4341 field_count: 0,
4342 fields: &[],
4343 },
4344 LayoutManifest {
4345 name: "Config",
4346 disc: 2,
4347 version: 1,
4348 layout_id: [8, 7, 6, 5, 4, 3, 2, 1],
4349 total_size: 43,
4350 field_count: 0,
4351 fields: &[],
4352 },
4353 ];
4354
4355 static PM_INSTRUCTIONS: &[InstructionDescriptor] = &[
4356 InstructionDescriptor {
4357 name: "deposit",
4358 tag: 1,
4359 args: &[],
4360 accounts: &[],
4361 capabilities: &["MutatesState"],
4362 policy_pack: "TREASURY_WRITE",
4363 receipt_expected: true,
4364 },
4365 InstructionDescriptor {
4366 name: "withdraw",
4367 tag: 2,
4368 args: &[],
4369 accounts: &[],
4370 capabilities: &["MutatesState", "TransfersTokens"],
4371 policy_pack: "TREASURY_WRITE",
4372 receipt_expected: true,
4373 },
4374 ];
4375
4376 static PM_POLICIES: &[PolicyDescriptor] = &[PolicyDescriptor {
4377 name: "TREASURY_WRITE",
4378 capabilities: &["MutatesState"],
4379 requirements: &["SignerAuthority"],
4380 invariants: &[],
4381 receipt_profile: "default-mutation",
4382 }];
4383
4384 #[test]
4385 fn program_manifest_find_layout_by_disc() {
4386 let prog = ProgramManifest {
4387 name: "test",
4388 version: "0.1.0",
4389 description: "",
4390 layouts: PM_LAYOUTS,
4391 layout_metadata: &[],
4392 instructions: &[],
4393 events: &[],
4394 policies: &[],
4395 compatibility_pairs: &[],
4396 tooling_hints: &[],
4397 contexts: &[],
4398 };
4399 assert_eq!(prog.layout_count(), 2);
4400 assert!(prog.find_layout_by_disc(1).is_some());
4401 assert_eq!(prog.find_layout_by_disc(1).unwrap().name, "Vault");
4402 assert!(prog.find_layout_by_disc(2).is_some());
4403 assert!(prog.find_layout_by_disc(3).is_none());
4404 }
4405
4406 #[test]
4407 fn program_manifest_find_layout_by_id() {
4408 let prog = ProgramManifest {
4409 name: "test",
4410 version: "0.1.0",
4411 description: "",
4412 layouts: PM_LAYOUTS,
4413 layout_metadata: &[],
4414 instructions: &[],
4415 events: &[],
4416 policies: &[],
4417 compatibility_pairs: &[],
4418 tooling_hints: &[],
4419 contexts: &[],
4420 };
4421 let id = [1, 2, 3, 4, 5, 6, 7, 8];
4422 assert!(prog.find_layout_by_id(&id).is_some());
4423 let bad_id = [0, 0, 0, 0, 0, 0, 0, 0];
4424 assert!(prog.find_layout_by_id(&bad_id).is_none());
4425 }
4426
4427 #[test]
4428 fn program_manifest_identify_from_data() {
4429 static ID_LAYOUTS: &[LayoutManifest] = &[LayoutManifest {
4430 name: "Vault",
4431 disc: 1,
4432 version: 1,
4433 layout_id: [0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80],
4434 total_size: 57,
4435 field_count: 0,
4436 fields: &[],
4437 }];
4438 let prog = ProgramManifest {
4439 name: "test",
4440 version: "0.1.0",
4441 description: "",
4442 layouts: ID_LAYOUTS,
4443 layout_metadata: &[],
4444 instructions: &[],
4445 events: &[],
4446 policies: &[],
4447 compatibility_pairs: &[],
4448 tooling_hints: &[],
4449 contexts: &[],
4450 };
4451 let mut data = [0u8; 57];
4453 data[0] = 1; data[1] = 1; data[4..12].copy_from_slice(&[0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80]);
4456 let result = prog.identify_from_data(&data);
4457 assert!(result.is_some());
4458 assert_eq!(result.unwrap().name, "Vault");
4459 }
4460
4461 #[test]
4462 fn program_manifest_find_instruction() {
4463 let prog = ProgramManifest {
4464 name: "test",
4465 version: "0.1.0",
4466 description: "",
4467 layouts: &[],
4468 layout_metadata: &[],
4469 instructions: PM_INSTRUCTIONS,
4470 events: &[],
4471 policies: &[],
4472 compatibility_pairs: &[],
4473 tooling_hints: &[],
4474 contexts: &[],
4475 };
4476 assert_eq!(prog.instruction_count(), 2);
4477 assert_eq!(prog.find_instruction(1).unwrap().name, "deposit");
4478 assert_eq!(prog.find_instruction(2).unwrap().name, "withdraw");
4479 assert!(prog.find_instruction(3).is_none());
4480 }
4481
4482 #[test]
4483 fn context_resolvers_and_effects_are_derived_from_manifest_metadata() {
4484 static CTX_ACCOUNTS: &[crate::accounts::ContextAccountDescriptor] = &[
4485 crate::accounts::ContextAccountDescriptor {
4486 name: "authority",
4487 kind: "Signer",
4488 writable: false,
4489 signer: true,
4490 layout_ref: "",
4491 policy_ref: "AUTHORITY",
4492 seeds: &[],
4493 optional: false,
4494 lifecycle: crate::accounts::AccountLifecycle::Existing,
4495 payer: "",
4496 init_space: 0,
4497 has_one: &[],
4498 expected_address: "",
4499 expected_owner: "",
4500 },
4501 crate::accounts::ContextAccountDescriptor {
4502 name: "vault",
4503 kind: "HopperAccount",
4504 writable: true,
4505 signer: false,
4506 layout_ref: "Vault",
4507 policy_ref: "TREASURY_WRITE",
4508 seeds: &["b\"vault\"", "authority"],
4509 optional: false,
4510 lifecycle: crate::accounts::AccountLifecycle::Init,
4511 payer: "authority",
4512 init_space: 128,
4513 has_one: &[],
4514 expected_address: "",
4515 expected_owner: "",
4516 },
4517 ];
4518 static CONTEXTS: &[crate::accounts::ContextDescriptor] =
4519 &[crate::accounts::ContextDescriptor {
4520 name: "deposit",
4521 accounts: CTX_ACCOUNTS,
4522 policies: &["TREASURY_WRITE"],
4523 receipts_expected: true,
4524 mutation_classes: &["Financial"],
4525 }];
4526
4527 let resolver = CONTEXTS[0].find_resolver("vault").unwrap();
4528 assert_eq!(resolver.kind, AccountResolverKind::Pda);
4529 assert_eq!(resolver.seeds.len(), 2);
4530 assert_eq!(resolver.payer, "authority");
4531
4532 let effect = CONTEXTS[0].find_effect("vault").unwrap();
4533 assert_eq!(effect.kind, InstructionEffectKind::CreatesAccount);
4534 assert_eq!(effect.layout_ref, "Vault");
4535 assert_eq!(CONTEXTS[0].effect_count(), 3);
4536
4537 let prog = ProgramManifest {
4538 name: "test",
4539 version: "0.1.0",
4540 description: "",
4541 layouts: &[],
4542 layout_metadata: &[],
4543 instructions: PM_INSTRUCTIONS,
4544 events: &[],
4545 policies: &[],
4546 compatibility_pairs: &[],
4547 tooling_hints: &[],
4548 contexts: CONTEXTS,
4549 };
4550 assert!(prog.find_context("deposit").is_some());
4551 extern crate alloc;
4552 use alloc::format;
4553 let rendered = format!("{}", prog);
4554 assert!(rendered.contains("resolvers=2 effects=3"));
4555 }
4556
4557 #[test]
4558 fn program_manifest_find_policy() {
4559 let prog = ProgramManifest {
4560 name: "test",
4561 version: "0.1.0",
4562 description: "",
4563 layouts: &[],
4564 layout_metadata: &[],
4565 instructions: &[],
4566 events: &[],
4567 policies: PM_POLICIES,
4568 compatibility_pairs: &[],
4569 tooling_hints: &[],
4570 contexts: &[],
4571 };
4572 assert!(prog.find_policy("TREASURY_WRITE").is_some());
4573 assert!(prog.find_policy("NONEXISTENT").is_none());
4574 }
4575
4576 #[test]
4577 fn decode_account_fields_basic() {
4578 static DECODE_FIELDS: &[FieldDescriptor] = &[
4579 FieldDescriptor {
4580 name: "balance",
4581 canonical_type: "WireU64",
4582 size: 8,
4583 offset: 16,
4584 intent: FieldIntent::Custom,
4585 },
4586 FieldDescriptor {
4587 name: "bump",
4588 canonical_type: "u8",
4589 size: 1,
4590 offset: 24,
4591 intent: FieldIntent::Custom,
4592 },
4593 ];
4594 static DECODE_MANIFEST: LayoutManifest = LayoutManifest {
4595 name: "Test",
4596 disc: 1,
4597 version: 1,
4598 layout_id: [0; 8],
4599 total_size: 25,
4600 field_count: 2,
4601 fields: DECODE_FIELDS,
4602 };
4603 let mut data = [0u8; 25];
4604 let balance_bytes = 1000u64.to_le_bytes();
4605 data[16..24].copy_from_slice(&balance_bytes);
4606 data[24] = 254;
4607
4608 let (count, decoded) = decode_account_fields::<8>(&data, &DECODE_MANIFEST);
4609 assert_eq!(count, 2);
4610 assert!(decoded[0].is_some());
4611 assert_eq!(decoded[0].as_ref().unwrap().name, "balance");
4612 assert!(decoded[1].is_some());
4613 assert_eq!(decoded[1].as_ref().unwrap().name, "bump");
4614 assert_eq!(decoded[1].as_ref().unwrap().raw[0], 254);
4615 }
4616
4617 #[test]
4618 fn decoded_field_format_wire_u64() {
4619 let raw = 42u64.to_le_bytes();
4620 let field = DecodedField {
4621 name: "balance",
4622 canonical_type: "WireU64",
4623 raw: &raw,
4624 offset: 16,
4625 size: 8,
4626 };
4627 let mut buf = [0u8; 32];
4628 let len = field.format_value(&mut buf);
4629 assert_eq!(&buf[..len], b"42");
4630 }
4631
4632 #[test]
4633 fn decoded_field_format_wire_u32() {
4634 let raw = 65535u32.to_le_bytes();
4635 let field = DecodedField {
4636 name: "count",
4637 canonical_type: "WireU32",
4638 raw: &raw,
4639 offset: 0,
4640 size: 4,
4641 };
4642 let mut buf = [0u8; 32];
4643 let len = field.format_value(&mut buf);
4644 assert_eq!(&buf[..len], b"65535");
4645 }
4646
4647 #[test]
4648 fn decoded_field_format_bool() {
4649 let raw_true = [1u8];
4650 let field = DecodedField {
4651 name: "frozen",
4652 canonical_type: "WireBool",
4653 raw: &raw_true,
4654 offset: 0,
4655 size: 1,
4656 };
4657 let mut buf = [0u8; 32];
4658 let len = field.format_value(&mut buf);
4659 assert_eq!(&buf[..len], b"true");
4660
4661 let raw_false = [0u8];
4662 let field2 = DecodedField {
4663 name: "frozen",
4664 canonical_type: "WireBool",
4665 raw: &raw_false,
4666 offset: 0,
4667 size: 1,
4668 };
4669 let len = field2.format_value(&mut buf);
4670 assert_eq!(&buf[..len], b"false");
4671 }
4672
4673 #[test]
4674 fn decoded_field_format_address() {
4675 let raw = [0xABu8; 32];
4676 let field = DecodedField {
4677 name: "authority",
4678 canonical_type: "[u8;32]",
4679 raw: &raw,
4680 offset: 0,
4681 size: 32,
4682 };
4683 let mut buf = [0u8; 64];
4684 let len = field.format_value(&mut buf);
4685 let s = core::str::from_utf8(&buf[..len]).unwrap();
4686 assert!(s.starts_with("0x"));
4687 assert!(s.ends_with("..."));
4688 }
4689
4690 #[test]
4691 fn format_u64_basic() {
4692 let mut buf = [0u8; 32];
4693 let len = super::format_u64(12345, &mut buf);
4694 assert_eq!(&buf[..len], b"12345");
4695
4696 let len = super::format_u64(0, &mut buf);
4697 assert_eq!(&buf[..len], b"0");
4698
4699 let len = super::format_u64(u64::MAX, &mut buf);
4700 let expected = b"18446744073709551615";
4701 assert_eq!(&buf[..len], &expected[..]);
4702 }
4703
4704 #[test]
4705 fn format_hex_truncated_short() {
4706 let mut buf = [0u8; 64];
4707 let len = super::format_hex_truncated(&[0xAB, 0xCD], &mut buf);
4708 assert_eq!(&buf[..len], b"0xabcd");
4709 }
4710
4711 #[test]
4712 fn format_hex_truncated_long() {
4713 let mut buf = [0u8; 64];
4714 let data = [0xFFu8; 32];
4715 let len = super::format_hex_truncated(&data, &mut buf);
4716 let s = core::str::from_utf8(&buf[..len]).unwrap();
4717 assert!(s.starts_with("0x"));
4718 assert!(s.ends_with("..."));
4719 assert_eq!(len, 21); }
4721
4722 #[test]
4723 fn program_manifest_display() {
4724 let prog = ProgramManifest {
4725 name: "test_program",
4726 version: "0.1.0",
4727 description: "A test",
4728 layouts: PM_LAYOUTS,
4729 layout_metadata: &[],
4730 instructions: PM_INSTRUCTIONS,
4731 events: &[],
4732 policies: PM_POLICIES,
4733 compatibility_pairs: &[],
4734 tooling_hints: &[],
4735 contexts: &[],
4736 };
4737 extern crate alloc;
4738 use alloc::format;
4739 let s = format!("{}", prog);
4740 assert!(s.contains("test_program"));
4741 assert!(s.contains("Vault"));
4742 assert!(s.contains("deposit"));
4743 assert!(s.contains("MutatesState"));
4744 assert!(s.contains("TREASURY_WRITE"));
4745 assert!(s.contains("SignerAuthority"));
4746 }
4747
4748 #[test]
4749 fn program_manifest_empty() {
4750 let prog = ProgramManifest::empty();
4751 assert_eq!(prog.layout_count(), 0);
4752 assert_eq!(prog.instruction_count(), 0);
4753 assert!(prog.find_layout_by_disc(0).is_none());
4754 assert!(prog.find_instruction(0).is_none());
4755 assert!(prog.identify_from_data(&[0u8; 16]).is_none());
4756 }
4757
4758 #[test]
4763 fn decode_header_empty_buffer() {
4764 assert!(decode_header(&[]).is_none());
4765 }
4766
4767 #[test]
4768 fn decode_header_one_byte() {
4769 assert!(decode_header(&[0xFF]).is_none());
4770 }
4771
4772 #[test]
4773 fn decode_header_fifteen_bytes() {
4774 assert!(decode_header(&[0u8; 15]).is_none());
4775 }
4776
4777 #[test]
4778 fn decode_header_exact_sixteen() {
4779 let h = decode_header(&[0u8; 16]);
4780 assert!(h.is_some());
4781 let h = h.unwrap();
4782 assert_eq!(h.disc, 0);
4783 assert_eq!(h.version, 0);
4784 }
4785
4786 #[test]
4787 fn decode_header_large_buffer() {
4788 let data = [0xABu8; 1024];
4789 let h = decode_header(&data).unwrap();
4790 assert_eq!(h.disc, 0xAB);
4791 assert_eq!(h.version, 0xAB);
4792 }
4793
4794 #[test]
4795 fn decode_segments_too_short() {
4796 assert!(decode_segments::<8>(&[0u8; 19]).is_none());
4798 }
4799
4800 #[test]
4801 fn decode_segments_zero_count() {
4802 let mut data = [0u8; 20];
4804 data[16] = 0; data[17] = 0; let result = decode_segments::<8>(&data);
4807 assert!(result.is_some());
4808 let (n, _) = result.unwrap();
4809 assert_eq!(n, 0);
4810 }
4811
4812 #[test]
4813 fn compare_fields_identical_empty() {
4814 let a = LayoutManifest {
4815 name: "A",
4816 disc: 1,
4817 version: 1,
4818 layout_id: [0; 8],
4819 total_size: 16,
4820 field_count: 0,
4821 fields: &[],
4822 };
4823 let b = LayoutManifest {
4824 name: "B",
4825 disc: 1,
4826 version: 1,
4827 layout_id: [0; 8],
4828 total_size: 16,
4829 field_count: 0,
4830 fields: &[],
4831 };
4832 let report = compare_fields::<8>(&a, &b);
4833 assert_eq!(report.count, 0);
4834 assert!(report.is_append_safe);
4835 }
4836
4837 static SINGLE_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4838 name: "x",
4839 canonical_type: "u8",
4840 size: 1,
4841 offset: 16,
4842 intent: FieldIntent::Custom,
4843 }];
4844
4845 #[test]
4846 fn compare_fields_all_removed() {
4847 let a = LayoutManifest {
4848 name: "A",
4849 disc: 1,
4850 version: 1,
4851 layout_id: [1; 8],
4852 total_size: 17,
4853 field_count: 1,
4854 fields: SINGLE_FIELD,
4855 };
4856 let b = LayoutManifest {
4857 name: "B",
4858 disc: 1,
4859 version: 2,
4860 layout_id: [2; 8],
4861 total_size: 16,
4862 field_count: 0,
4863 fields: &[],
4864 };
4865 let report = compare_fields::<8>(&a, &b);
4866 assert_eq!(report.count, 1);
4867 assert!(!report.is_append_safe);
4868 }
4869
4870 static OLD_TYPE_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4871 name: "x",
4872 canonical_type: "u8",
4873 size: 1,
4874 offset: 16,
4875 intent: FieldIntent::Custom,
4876 }];
4877 static NEW_TYPE_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
4878 name: "x",
4879 canonical_type: "u16",
4880 size: 2,
4881 offset: 16,
4882 intent: FieldIntent::Custom,
4883 }];
4884
4885 #[test]
4886 fn compare_fields_type_change_detected() {
4887 let a = LayoutManifest {
4888 name: "A",
4889 disc: 1,
4890 version: 1,
4891 layout_id: [1; 8],
4892 total_size: 17,
4893 field_count: 1,
4894 fields: OLD_TYPE_FIELD,
4895 };
4896 let b = LayoutManifest {
4897 name: "B",
4898 disc: 1,
4899 version: 2,
4900 layout_id: [2; 8],
4901 total_size: 18,
4902 field_count: 1,
4903 fields: NEW_TYPE_FIELD,
4904 };
4905 let report = compare_fields::<8>(&a, &b);
4906 assert_eq!(report.entries[0].status, FieldCompat::Changed);
4907 assert!(!report.is_append_safe);
4908 }
4909
4910 #[test]
4911 fn verdict_different_disc_is_incompatible() {
4912 let a = LayoutManifest {
4913 name: "A",
4914 disc: 1,
4915 version: 1,
4916 layout_id: [1; 8],
4917 total_size: 16,
4918 field_count: 0,
4919 fields: &[],
4920 };
4921 let b = LayoutManifest {
4922 name: "B",
4923 disc: 2,
4924 version: 1,
4925 layout_id: [2; 8],
4926 total_size: 16,
4927 field_count: 0,
4928 fields: &[],
4929 };
4930 assert_eq!(
4931 CompatibilityVerdict::between(&a, &b),
4932 CompatibilityVerdict::Incompatible
4933 );
4934 }
4935
4936 #[test]
4937 fn verdict_same_id_is_identical() {
4938 let a = LayoutManifest {
4939 name: "A",
4940 disc: 1,
4941 version: 1,
4942 layout_id: [9; 8],
4943 total_size: 16,
4944 field_count: 0,
4945 fields: &[],
4946 };
4947 assert_eq!(
4948 CompatibilityVerdict::between(&a, &a),
4949 CompatibilityVerdict::Identical
4950 );
4951 }
4952
4953 #[test]
4954 fn compatibility_explain_between_identical() {
4955 let a = LayoutManifest {
4956 name: "A",
4957 disc: 1,
4958 version: 1,
4959 layout_id: [9; 8],
4960 total_size: 16,
4961 field_count: 0,
4962 fields: &[],
4963 };
4964 let exp = CompatibilityExplain::between(&a, &a);
4965 assert_eq!(exp.verdict, CompatibilityVerdict::Identical);
4966 assert_eq!(exp.added_count, 0);
4967 assert_eq!(exp.removed_count, 0);
4968 assert!(!exp.semantic_drift);
4969 }
4970
4971 static APPEND_OLD: &[FieldDescriptor] = &[FieldDescriptor {
4972 name: "a",
4973 canonical_type: "u8",
4974 size: 1,
4975 offset: 16,
4976 intent: FieldIntent::Custom,
4977 }];
4978 static APPEND_NEW: &[FieldDescriptor] = &[
4979 FieldDescriptor {
4980 name: "a",
4981 canonical_type: "u8",
4982 size: 1,
4983 offset: 16,
4984 intent: FieldIntent::Custom,
4985 },
4986 FieldDescriptor {
4987 name: "b",
4988 canonical_type: "u8",
4989 size: 1,
4990 offset: 17,
4991 intent: FieldIntent::Custom,
4992 },
4993 ];
4994
4995 #[test]
4996 fn compatibility_explain_append_counts_fields() {
4997 let older = LayoutManifest {
4998 name: "T",
4999 disc: 1,
5000 version: 1,
5001 layout_id: [1; 8],
5002 total_size: 17,
5003 field_count: 1,
5004 fields: APPEND_OLD,
5005 };
5006 let newer = LayoutManifest {
5007 name: "T",
5008 disc: 1,
5009 version: 2,
5010 layout_id: [2; 8],
5011 total_size: 18,
5012 field_count: 2,
5013 fields: APPEND_NEW,
5014 };
5015 let exp = CompatibilityExplain::between(&older, &newer);
5016 assert_eq!(exp.verdict, CompatibilityVerdict::AppendSafe);
5017 assert_eq!(exp.added_count, 1);
5018 assert_eq!(exp.added_fields[0], "b");
5019 }
5020
5021 #[test]
5022 fn layout_fingerprint_deterministic() {
5023 let m = LayoutManifest {
5024 name: "X",
5025 disc: 1,
5026 version: 1,
5027 layout_id: [5; 8],
5028 total_size: 16,
5029 field_count: 0,
5030 fields: &[],
5031 };
5032 let fp1 = LayoutFingerprint::from_manifest(&m);
5033 let fp2 = LayoutFingerprint::from_manifest(&m);
5034 assert_eq!(fp1.wire_hash, fp2.wire_hash);
5035 assert_eq!(fp1.semantic_hash, fp2.semantic_hash);
5036 }
5037
5038 static FP_CUSTOM: &[FieldDescriptor] = &[FieldDescriptor {
5039 name: "x",
5040 canonical_type: "u8",
5041 size: 1,
5042 offset: 16,
5043 intent: FieldIntent::Custom,
5044 }];
5045 static FP_BALANCE: &[FieldDescriptor] = &[FieldDescriptor {
5046 name: "x",
5047 canonical_type: "u8",
5048 size: 1,
5049 offset: 16,
5050 intent: FieldIntent::Balance,
5051 }];
5052
5053 #[test]
5054 fn layout_fingerprint_differs_on_intent_change() {
5055 let m1 = LayoutManifest {
5056 name: "T",
5057 disc: 1,
5058 version: 1,
5059 layout_id: [1; 8],
5060 total_size: 17,
5061 field_count: 1,
5062 fields: FP_CUSTOM,
5063 };
5064 let m2 = LayoutManifest {
5065 name: "T",
5066 disc: 1,
5067 version: 1,
5068 layout_id: [1; 8],
5069 total_size: 17,
5070 field_count: 1,
5071 fields: FP_BALANCE,
5072 };
5073 let fp1 = LayoutFingerprint::from_manifest(&m1);
5074 let fp2 = LayoutFingerprint::from_manifest(&m2);
5075 assert_eq!(fp1.wire_hash, fp2.wire_hash);
5076 assert_ne!(fp1.semantic_hash, fp2.semantic_hash);
5077 }
5078
5079 static LINT_AUTH_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
5080 name: "auth",
5081 canonical_type: "[u8;32]",
5082 size: 32,
5083 offset: 16,
5084 intent: FieldIntent::Authority,
5085 }];
5086
5087 #[test]
5088 fn lint_layout_authority_without_signer() {
5089 let m = LayoutManifest {
5090 name: "T",
5091 disc: 1,
5092 version: 1,
5093 layout_id: [0; 8],
5094 total_size: 48,
5095 field_count: 1,
5096 fields: LINT_AUTH_FIELD,
5097 };
5098 let behavior = LayoutBehavior {
5100 requires_signer: false,
5101 affects_balance: false,
5102 affects_authority: true,
5103 mutation_class: MutationClass::InPlace,
5104 };
5105 let (n, lints) = lint_layout::<8>(&m, &behavior);
5106 assert!(n >= 1);
5107 assert_eq!(lints[0].code, "E001");
5108 }
5109
5110 #[test]
5111 fn lint_layout_clean_passes() {
5112 let m = LayoutManifest {
5113 name: "T",
5114 disc: 1,
5115 version: 1,
5116 layout_id: [0; 8],
5117 total_size: 48,
5118 field_count: 1,
5119 fields: LINT_AUTH_FIELD,
5120 };
5121 let behavior = LayoutBehavior {
5122 requires_signer: true,
5123 affects_balance: false,
5124 affects_authority: true,
5125 mutation_class: MutationClass::AuthoritySensitive,
5126 };
5127 let (n, _) = lint_layout::<8>(&m, &behavior);
5128 assert_eq!(n, 0);
5129 }
5130
5131 #[test]
5132 fn mutation_class_properties() {
5133 assert!(!MutationClass::ReadOnly.is_mutating());
5134 assert!(MutationClass::InPlace.is_mutating());
5135 assert!(MutationClass::Financial.requires_snapshot());
5136 assert!(MutationClass::AuthoritySensitive.requires_authority());
5137 assert!(!MutationClass::AppendOnly.requires_authority());
5138 }
5139
5140 static SEED_FIELD: &[FieldDescriptor] = &[FieldDescriptor {
5141 name: "seed",
5142 canonical_type: "[u8;32]",
5143 size: 32,
5144 offset: 16,
5145 intent: FieldIntent::PDASeed,
5146 }];
5147
5148 #[test]
5149 fn layout_stability_grade_stable_with_init_only() {
5150 let m = LayoutManifest {
5151 name: "T",
5152 disc: 1,
5153 version: 1,
5154 layout_id: [0; 8],
5155 total_size: 48,
5156 field_count: 1,
5157 fields: SEED_FIELD,
5158 };
5159 assert_eq!(
5160 LayoutStabilityGrade::compute(&m),
5161 LayoutStabilityGrade::Stable
5162 );
5163 }
5164
5165 #[test]
5166 fn layout_stability_grade_evolving_with_custom() {
5167 let m = LayoutManifest {
5168 name: "T",
5169 disc: 1,
5170 version: 1,
5171 layout_id: [0; 8],
5172 total_size: 17,
5173 field_count: 1,
5174 fields: SINGLE_FIELD,
5175 };
5176 assert_eq!(
5177 LayoutStabilityGrade::compute(&m),
5178 LayoutStabilityGrade::Evolving
5179 );
5180 }
5181
5182 static GRADE_HEAVY: &[FieldDescriptor] = &[
5183 FieldDescriptor {
5184 name: "auth1",
5185 canonical_type: "[u8;32]",
5186 size: 32,
5187 offset: 16,
5188 intent: FieldIntent::Authority,
5189 },
5190 FieldDescriptor {
5191 name: "auth2",
5192 canonical_type: "[u8;32]",
5193 size: 32,
5194 offset: 48,
5195 intent: FieldIntent::Owner,
5196 },
5197 FieldDescriptor {
5198 name: "auth3",
5199 canonical_type: "[u8;32]",
5200 size: 32,
5201 offset: 80,
5202 intent: FieldIntent::Delegate,
5203 },
5204 FieldDescriptor {
5205 name: "bal1",
5206 canonical_type: "WireU64",
5207 size: 8,
5208 offset: 112,
5209 intent: FieldIntent::Balance,
5210 },
5211 FieldDescriptor {
5212 name: "bal2",
5213 canonical_type: "WireU64",
5214 size: 8,
5215 offset: 120,
5216 intent: FieldIntent::Supply,
5217 },
5218 FieldDescriptor {
5219 name: "bal3",
5220 canonical_type: "WireU64",
5221 size: 8,
5222 offset: 128,
5223 intent: FieldIntent::Balance,
5224 },
5225 ];
5226
5227 #[test]
5228 fn layout_stability_grade_unsafe_to_evolve_heavy() {
5229 let m = LayoutManifest {
5230 name: "T",
5231 disc: 1,
5232 version: 1,
5233 layout_id: [0; 8],
5234 total_size: 136,
5235 field_count: 6,
5236 fields: GRADE_HEAVY,
5237 };
5238 let grade = LayoutStabilityGrade::compute(&m);
5239 assert_eq!(grade, LayoutStabilityGrade::UnsafeToEvolve);
5240 }
5241
5242 #[test]
5243 fn field_intent_new_variants_coverage() {
5244 assert_eq!(FieldIntent::PDASeed.name(), "pda_seed");
5245 assert_eq!(FieldIntent::Version.name(), "version");
5246 assert_eq!(FieldIntent::Bump.name(), "bump");
5247 assert_eq!(FieldIntent::Status.name(), "status");
5248 assert!(FieldIntent::Owner.is_authority_sensitive());
5249 assert!(FieldIntent::Delegate.is_authority_sensitive());
5250 assert!(FieldIntent::Threshold.is_governance());
5251 assert!(FieldIntent::Bump.is_init_only());
5252 assert!(FieldIntent::PDASeed.is_init_only());
5253 assert!(FieldIntent::Supply.is_monetary());
5254 }
5255
5256 #[test]
5257 fn refine_verdict_softens_with_rebuildable_segments() {
5258 let advice = [
5259 SegmentAdvice {
5260 id: [0; 4],
5261 size: 100,
5262 role: SegmentRoleHint::Cache,
5263 must_preserve: false,
5264 clearable: true,
5265 rebuildable: true,
5266 append_only: false,
5267 immutable: false,
5268 },
5269 SegmentAdvice {
5270 id: [0; 4],
5271 size: 0,
5272 role: SegmentRoleHint::Unclassified,
5273 must_preserve: false,
5274 clearable: false,
5275 rebuildable: false,
5276 append_only: false,
5277 immutable: false,
5278 },
5279 ];
5280 let report = SegmentMigrationReport {
5281 advice,
5282 count: 1,
5283 preserve_bytes: 0,
5284 clearable_bytes: 100,
5285 rebuildable_bytes: 100,
5286 };
5287 let refined = CompatibilityVerdict::MigrationRequired.refine_with_roles(&report);
5288 assert_eq!(refined, CompatibilityVerdict::AppendSafe);
5289 }
5290
5291 #[test]
5292 fn refine_verdict_escalates_with_immutable_segment() {
5293 let advice = [SegmentAdvice {
5294 id: [0; 4],
5295 size: 50,
5296 role: SegmentRoleHint::Audit,
5297 must_preserve: true,
5298 clearable: false,
5299 rebuildable: false,
5300 append_only: true,
5301 immutable: true,
5302 }];
5303 let report = SegmentMigrationReport {
5304 advice,
5305 count: 1,
5306 preserve_bytes: 50,
5307 clearable_bytes: 0,
5308 rebuildable_bytes: 0,
5309 };
5310 let refined = CompatibilityVerdict::AppendSafe.refine_with_roles(&report);
5311 assert_eq!(refined, CompatibilityVerdict::MigrationRequired);
5312 }
5313
5314 #[test]
5315 #[cfg(feature = "policy")]
5316 fn lint_policy_financial_mismatch() {
5317 let behavior = LayoutBehavior {
5318 requires_signer: true,
5319 affects_balance: true,
5320 affects_authority: false,
5321 mutation_class: MutationClass::Financial,
5322 };
5323 let (n, lints) = lint_policy::<8>(&behavior, hopper_core::policy::PolicyClass::Write);
5324 assert!(n >= 1);
5325 assert_eq!(lints[0].code, "W005");
5326 }
5327
5328 #[test]
5329 #[cfg(feature = "policy")]
5330 fn lint_policy_reverse_mismatch() {
5331 let behavior = LayoutBehavior {
5332 requires_signer: true,
5333 affects_balance: false,
5334 affects_authority: false,
5335 mutation_class: MutationClass::InPlace,
5336 };
5337 let (n, lints) = lint_policy::<8>(&behavior, hopper_core::policy::PolicyClass::Financial);
5338 assert!(n >= 1);
5339 assert_eq!(lints[0].code, "W006");
5340 }
5341
5342 #[test]
5343 #[cfg(feature = "policy")]
5344 fn lint_policy_clean_when_aligned() {
5345 let behavior = LayoutBehavior {
5346 requires_signer: true,
5347 affects_balance: true,
5348 affects_authority: false,
5349 mutation_class: MutationClass::Financial,
5350 };
5351 let (n, _) = lint_policy::<8>(&behavior, hopper_core::policy::PolicyClass::Financial);
5352 assert_eq!(n, 0);
5353 }
5354
5355 #[test]
5356 fn display_field_intent() {
5357 extern crate alloc;
5358 use alloc::format;
5359 assert_eq!(format!("{}", FieldIntent::Balance), "balance");
5360 assert_eq!(format!("{}", FieldIntent::Authority), "authority");
5361 }
5362
5363 #[test]
5364 fn display_mutation_class() {
5365 extern crate alloc;
5366 use alloc::format;
5367 assert_eq!(format!("{}", MutationClass::Financial), "financial");
5368 assert_eq!(format!("{}", MutationClass::ReadOnly), "read-only");
5369 }
5370
5371 #[test]
5372 fn display_layout_stability_grade() {
5373 extern crate alloc;
5374 use alloc::format;
5375 assert_eq!(format!("{}", LayoutStabilityGrade::Stable), "stable");
5376 assert_eq!(
5377 format!("{}", LayoutStabilityGrade::UnsafeToEvolve),
5378 "unsafe-to-evolve"
5379 );
5380 }
5381
5382 #[test]
5383 fn display_compatibility_verdict() {
5384 extern crate alloc;
5385 use alloc::format;
5386 assert_eq!(format!("{}", CompatibilityVerdict::Identical), "identical");
5387 assert_eq!(
5388 format!("{}", CompatibilityVerdict::MigrationRequired),
5389 "migration-required"
5390 );
5391 }
5392
5393 #[test]
5394 fn display_layout_fingerprint() {
5395 extern crate alloc;
5396 use alloc::format;
5397 let fp = LayoutFingerprint {
5398 wire_hash: [0xAB, 0xCD, 0, 0, 0, 0, 0, 0],
5399 semantic_hash: [0, 0, 0, 0, 0, 0, 0xFF, 0x01],
5400 };
5401 let s = format!("{}", fp);
5402 assert!(s.starts_with("wire=abcd"));
5403 assert!(s.contains("sem="));
5404 assert!(s.ends_with("ff01"));
5405 }
5406
5407 #[test]
5408 fn display_receipt_profile() {
5409 extern crate alloc;
5410 use alloc::format;
5411 let rp = ReceiptProfile {
5412 name: "test",
5413 expected_phase: "Mutate",
5414 expects_balance_change: true,
5415 expects_authority_change: false,
5416 expects_journal_append: false,
5417 min_changed_fields: 2,
5418 };
5419 let s = format!("{}", rp);
5420 assert!(s.contains("test"));
5421 assert!(s.contains("Mutate"));
5422 assert!(s.contains("balance"));
5423 assert!(s.contains("min_fields=2"));
5424 }
5425
5426 #[test]
5427 fn display_idl_segment_descriptor() {
5428 extern crate alloc;
5429 use alloc::format;
5430 let sd = IdlSegmentDescriptor {
5431 name: "core",
5432 role: "Core",
5433 append_only: false,
5434 rebuildable: false,
5435 must_preserve: true,
5436 };
5437 let s = format!("{}", sd);
5438 assert!(s.contains("core"));
5439 assert!(s.contains("Core"));
5440 assert!(s.contains("must-preserve"));
5441 assert!(!s.contains("append-only"));
5442 }
5443}