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