1use hopper_schema::{FieldDescriptor, LayoutManifest};
11
12#[cfg(feature = "std")]
13use alloc::vec::Vec;
14
15#[derive(Debug, Clone, Copy)]
17pub struct FieldDelta<'a> {
18 pub field: &'a FieldDescriptor,
20 pub before: &'a [u8],
22 pub after: &'a [u8],
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum DiffError {
29 LengthMismatch {
32 before: usize,
34 after: usize,
36 },
37 BufferTooShort,
39}
40
41#[cfg(feature = "std")]
48pub fn fixed_size_diff<'a>(
49 before: &'a [u8],
50 after: &'a [u8],
51 manifest: &'a LayoutManifest,
52) -> Result<Vec<FieldDelta<'a>>, DiffError> {
53 if before.len() != after.len() {
54 return Err(DiffError::LengthMismatch {
55 before: before.len(),
56 after: after.len(),
57 });
58 }
59 if before.len() < manifest.total_size {
60 return Err(DiffError::BufferTooShort);
61 }
62 let mut out = Vec::new();
63 let mut i = 0;
64 while i < manifest.fields.len() {
65 let f = &manifest.fields[i];
66 let start = f.offset as usize;
67 let end = start + f.size as usize;
68 if end > before.len() {
69 break;
70 }
71 if before[start..end] != after[start..end] {
72 out.push(FieldDelta {
73 field: f,
74 before: &before[start..end],
75 after: &after[start..end],
76 });
77 }
78 i += 1;
79 }
80 Ok(out)
81}
82
83pub fn field_change_mask(before: &[u8], after: &[u8], manifest: &LayoutManifest) -> u64 {
87 let mut mask = 0u64;
88 let common = core::cmp::min(before.len(), after.len());
89 let mut i = 0;
90 while i < manifest.fields.len() && i < 64 {
91 let f = &manifest.fields[i];
92 let start = f.offset as usize;
93 let end = start + f.size as usize;
94 if end > common {
95 break;
96 }
97 if before[start..end] != after[start..end] {
98 mask |= 1u64 << i;
99 }
100 i += 1;
101 }
102 mask
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use hopper_schema::FieldIntent;
109
110 fn fields() -> &'static [FieldDescriptor] {
111 static F: [FieldDescriptor; 2] = [
112 FieldDescriptor {
113 name: "a",
114 canonical_type: "u64",
115 size: 8,
116 offset: 0,
117 intent: FieldIntent::Counter,
118 },
119 FieldDescriptor {
120 name: "b",
121 canonical_type: "u64",
122 size: 8,
123 offset: 8,
124 intent: FieldIntent::Balance,
125 },
126 ];
127 &F
128 }
129
130 fn manifest() -> LayoutManifest {
131 LayoutManifest {
132 name: "Pair",
133 disc: 1,
134 version: 1,
135 layout_id: [0; 8],
136 total_size: 16,
137 field_count: 2,
138 fields: fields(),
139 }
140 }
141
142 #[test]
143 fn mask_detects_changed_field() {
144 let mut before = [0u8; 16];
145 let mut after = [0u8; 16];
146 before[8..16].copy_from_slice(&1u64.to_le_bytes());
147 after[8..16].copy_from_slice(&2u64.to_le_bytes());
148 let m = manifest();
149 assert_eq!(field_change_mask(&before, &after, &m), 0b10);
150 }
151
152 #[cfg(feature = "std")]
153 #[test]
154 fn fixed_size_diff_returns_deltas() {
155 let before = [0u8; 16];
156 let mut after = [0u8; 16];
157 after[0..8].copy_from_slice(&5u64.to_le_bytes());
158 let m = manifest();
159 let d = fixed_size_diff(&before, &after, &m).unwrap();
160 assert_eq!(d.len(), 1);
161 assert_eq!(d[0].field.name, "a");
162 }
163}