1use std::collections::HashSet;
7
8use prikk_error::{PrikkError, Result};
9use prikk_object::{BlockKind, BlockPayload, ObjectId, ObjectType, RefStatePayload};
10
11use crate::layout::RepositoryLayout;
12use crate::object_store::{ObjectReadSnapshot, ObjectReader};
13use crate::refs::RefStore;
14use crate::rollback_verify::verify_rollback_patch_envelope;
15
16pub const DEFAULT_HISTORY_LIMIT: usize = 20;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct RefHistory {
22 pub ref_name: String,
24 pub entries: Vec<HistoryEntry>,
26}
27
28impl RefHistory {
29 #[must_use]
31 pub fn is_empty(&self) -> bool {
32 self.entries.is_empty()
33 }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct HistoryEntry {
39 pub ref_state_id: ObjectId,
41 pub block_id: ObjectId,
43 pub update_seq: u64,
45 pub previous_ref_state_id: Option<ObjectId>,
47 pub block_kind: BlockKind,
49 pub parent_count: usize,
51 pub patch_count: usize,
53 pub required_attestation_count: usize,
55 pub rollback_patch_count: usize,
57 pub is_rollback_block: bool,
59}
60
61pub fn load_ref_history(
66 layout: &RepositoryLayout,
67 ref_name: &str,
68 limit: usize,
69) -> Result<RefHistory> {
70 let ref_store = RefStore::new(layout.clone());
71 let object_store = ObjectReadSnapshot::open(layout)?;
72 let mut current = ref_store.read_current_ref_state_id(ref_name)?;
73 let mut entries = Vec::new();
74 let mut seen = HashSet::new();
75
76 while let Some(ref_state_id) = current {
77 if entries.len() >= limit {
78 break;
79 }
80 if !seen.insert(ref_state_id) {
81 return Err(PrikkError::Integrity(format!(
82 "RefState chain for {ref_name} contains a cycle at {ref_state_id}"
83 )));
84 }
85 let ref_state = read_ref_state(&object_store, ref_state_id, ref_name)?;
86 let block = read_block(&object_store, ref_state.target_object_id)?;
87 let rollback_patch_count =
88 count_rollback_patches(&object_store, ref_state.target_object_id, &block.patch_ids)?;
89 entries.push(HistoryEntry {
90 ref_state_id,
91 block_id: ref_state.target_object_id,
92 update_seq: ref_state.update_seq,
93 previous_ref_state_id: ref_state.previous_ref_state_id,
94 block_kind: block.kind,
95 parent_count: block.parent_block_ids.len(),
96 patch_count: block.patch_ids.len(),
97 required_attestation_count: ref_state.required_attestation_ids.len(),
98 rollback_patch_count,
99 is_rollback_block: rollback_patch_count != 0,
100 });
101 current = ref_state.previous_ref_state_id;
102 }
103
104 Ok(RefHistory {
105 ref_name: ref_name.to_string(),
106 entries,
107 })
108}
109
110pub fn load_received_ref_history(
118 layout: &RepositoryLayout,
119 received_ref_name: &str,
120 limit: usize,
121) -> Result<RefHistory> {
122 let Some(pointer) = crate::received::read_received_pointer(layout, received_ref_name)? else {
123 return Ok(RefHistory {
124 ref_name: received_ref_name.to_string(),
125 entries: Vec::new(),
126 });
127 };
128 let Some(origin_ref_name) = received_ref_name.strip_prefix("remotes/") else {
129 return Err(PrikkError::InvalidName(format!(
130 "{received_ref_name} is not a received ref"
131 )));
132 };
133 let object_store = ObjectReadSnapshot::open(layout)?;
134 let mut current = Some(pointer.ref_state_id);
135 let mut entries = Vec::new();
136 let mut seen = HashSet::new();
137
138 while let Some(ref_state_id) = current {
139 if entries.len() >= limit {
140 break;
141 }
142 if !seen.insert(ref_state_id) {
143 return Err(PrikkError::Integrity(format!(
144 "RefState chain for {received_ref_name} contains a cycle at {ref_state_id}"
145 )));
146 }
147 let ref_state = read_ref_state(&object_store, ref_state_id, origin_ref_name)?;
148 let block = read_block(&object_store, ref_state.target_object_id)?;
149 let rollback_patch_count =
150 count_rollback_patches(&object_store, ref_state.target_object_id, &block.patch_ids)?;
151 entries.push(HistoryEntry {
152 ref_state_id,
153 block_id: ref_state.target_object_id,
154 update_seq: ref_state.update_seq,
155 previous_ref_state_id: ref_state.previous_ref_state_id,
156 block_kind: block.kind,
157 parent_count: block.parent_block_ids.len(),
158 patch_count: block.patch_ids.len(),
159 required_attestation_count: ref_state.required_attestation_ids.len(),
160 rollback_patch_count,
161 is_rollback_block: rollback_patch_count != 0,
162 });
163 current = ref_state.previous_ref_state_id;
164 }
165
166 Ok(RefHistory {
167 ref_name: received_ref_name.to_string(),
168 entries,
169 })
170}
171
172fn read_ref_state(
173 object_store: &impl ObjectReader,
174 ref_state_id: ObjectId,
175 ref_name: &str,
176) -> Result<RefStatePayload> {
177 let Some(envelope) = object_store.read_object(ref_state_id)? else {
178 return Err(PrikkError::Integrity(format!(
179 "history RefState {ref_state_id} is missing"
180 )));
181 };
182 if envelope.object_type != ObjectType::RefState {
183 return Err(PrikkError::Integrity(format!(
184 "history object {ref_state_id} is {}, expected RefState",
185 envelope.object_type
186 )));
187 }
188 let payload =
189 RefStatePayload::decode_canonical(&envelope.canonical_payload, envelope.schema_version)?;
190 if payload.ref_name != ref_name {
191 return Err(PrikkError::Integrity(format!(
192 "history RefState {ref_state_id} name mismatch: expected {ref_name}, got {}",
193 payload.ref_name
194 )));
195 }
196 Ok(payload)
197}
198
199fn count_rollback_patches(
200 object_store: &impl ObjectReader,
201 block_id: ObjectId,
202 patch_ids: &[ObjectId],
203) -> Result<usize> {
204 let mut count = 0_usize;
205 for patch_id in patch_ids {
206 let Some(envelope) = object_store.read_typed(*patch_id, ObjectType::Patch)? else {
207 return Err(PrikkError::Integrity(format!(
208 "history Block {block_id} references missing Patch {patch_id}"
209 )));
210 };
211 let context = format!("history Block {block_id} Patch {patch_id}");
212 if verify_rollback_patch_envelope(&envelope, &context)? {
213 count = count.checked_add(1).ok_or_else(|| {
214 PrikkError::Integrity("history rollback patch count overflow".to_string())
215 })?;
216 }
217 }
218 Ok(count)
219}
220
221fn read_block(object_store: &impl ObjectReader, block_id: ObjectId) -> Result<BlockPayload> {
222 let Some(envelope) = object_store.read_object(block_id)? else {
223 return Err(PrikkError::Integrity(format!(
224 "history Block {block_id} is missing"
225 )));
226 };
227 if envelope.object_type != ObjectType::Block {
228 return Err(PrikkError::Integrity(format!(
229 "history object {block_id} is {}, expected Block",
230 envelope.object_type
231 )));
232 }
233 BlockPayload::decode_canonical(&envelope.canonical_payload)
234}
235
236#[cfg(test)]
237mod tests;