heddle_pack/store/pack/
pack_index.rs1use std::collections::HashSet;
5
6use bytes::Bytes;
7
8use crate::store::{
9 Result,
10 pack::{
11 PackObjectId,
12 versioned_header::{HeaderChecksum, VersionedHeader},
13 },
14};
15
16pub(super) const INDEX_MAGIC: &[u8; 4] = b"LMI\0";
17pub(super) const INDEX_VERSION: u32 = 4;
18pub(super) const INDEX_ENTRY_LEN: usize = 32 + 8;
19const STATE_ID_OFFSET_TAG: u64 = 1 << 63;
20const ANNOTATED_TAG_OFFSET_TAG: u64 = 1 << 62;
21const PACK_OFFSET_MASK: u64 = !(STATE_ID_OFFSET_TAG | ANNOTATED_TAG_OFFSET_TAG);
22
23#[derive(Debug, Clone, Copy)]
25pub struct IndexEntry {
26 pub id: PackObjectId,
27 pub offset: u64,
28}
29
30#[derive(Debug)]
32pub struct PackIndex {
33 entries: Vec<IndexEntry>,
34 encoded: Option<EncodedIndex>,
35}
36
37#[derive(Debug)]
38struct EncodedIndex {
39 data: Bytes,
40 entries_start: usize,
41 count: usize,
42}
43
44impl PackIndex {
45 pub fn new() -> Self {
47 Self {
48 entries: Vec::new(),
49 encoded: None,
50 }
51 }
52
53 pub fn add(&mut self, id: PackObjectId, offset: u64) {
55 debug_assert!(self.encoded.is_none());
56 self.entries.push(IndexEntry { id, offset });
57 }
58
59 pub fn sort(&mut self) {
61 debug_assert!(self.encoded.is_none());
62 self.entries.sort_by_key(|e| e.id);
63 }
64
65 pub fn find(&self, id: &PackObjectId) -> Result<Option<u64>> {
67 let Some(encoded) = &self.encoded else {
68 return Ok(self
69 .entries
70 .binary_search_by_key(id, |entry| entry.id)
71 .ok()
72 .map(|index| self.entries[index].offset));
73 };
74 let mut low = 0;
75 let mut high = encoded.count;
76 while low < high {
77 let middle = low + (high - low) / 2;
78 let entry = encoded.entry(middle)?;
79 match entry.id.cmp(id) {
80 std::cmp::Ordering::Less => low = middle + 1,
81 std::cmp::Ordering::Greater => high = middle,
82 std::cmp::Ordering::Equal => return Ok(Some(entry.offset)),
83 }
84 }
85 Ok(None)
86 }
87
88 pub fn to_bytes(&self) -> Vec<u8> {
90 if let Some(encoded) = &self.encoded {
91 return encoded.data.to_vec();
92 }
93 let mut result = Vec::new();
94 index_header().write_vec(&mut result, self.entries.len() as u64);
95 for entry in &self.entries {
96 result.extend_from_slice(&encode_index_entry(entry.id, entry.offset));
97 }
98 result
99 }
100
101 pub fn from_bytes(data: &[u8]) -> Result<Self> {
103 Self::from_owned_bytes(Bytes::copy_from_slice(data))
104 }
105
106 pub fn from_owned_bytes(data: Bytes) -> Result<Self> {
107 verify_index_version(&data)?;
108 let header = index_header().verify(&data)?;
109 let count = header.count;
110 let max_entries = ((data.len() - header.header_len) / INDEX_ENTRY_LEN) as u64;
111 if count > max_entries {
112 return Err(crate::store::StoreError::InvalidObject(format!(
113 "Index entry count {} exceeds available data capacity {}",
114 count, max_entries
115 )));
116 }
117 let count = usize::try_from(count).map_err(|_| {
118 crate::store::StoreError::InvalidObject(
119 "Index entry count exceeds platform limits".to_string(),
120 )
121 })?;
122 Ok(Self {
123 entries: Vec::new(),
124 encoded: Some(EncodedIndex {
125 data,
126 entries_start: header.header_len,
127 count,
128 }),
129 })
130 }
131}
132
133impl EncodedIndex {
134 fn entry(&self, index: usize) -> Result<IndexEntry> {
135 let start = self.entries_start + index * INDEX_ENTRY_LEN;
136 let end = start + INDEX_ENTRY_LEN;
137 let bytes = self.data.get(start..end).ok_or_else(|| {
138 crate::store::StoreError::InvalidObject("Index data truncated".to_string())
139 })?;
140 decode_index_entry(bytes)
141 }
142}
143
144pub(super) fn encode_index_entry(id: PackObjectId, offset: u64) -> [u8; INDEX_ENTRY_LEN] {
145 assert!(
146 offset <= PACK_OFFSET_MASK,
147 "pack index offset exceeds the 62-bit format limit"
148 );
149 let mut bytes = [0u8; INDEX_ENTRY_LEN];
150 let tagged_offset = match id {
151 PackObjectId::Hash(hash) => {
152 bytes[..32].copy_from_slice(hash.as_bytes());
153 offset
154 }
155 PackObjectId::StateId(state_id) => {
156 bytes[..32].copy_from_slice(state_id.as_bytes());
157 offset | STATE_ID_OFFSET_TAG
158 }
159 PackObjectId::AnnotatedTag(hash) => {
160 bytes[..32].copy_from_slice(hash.as_bytes());
161 offset | ANNOTATED_TAG_OFFSET_TAG
162 }
163 };
164 bytes[32..].copy_from_slice(&tagged_offset.to_be_bytes());
165 bytes
166}
167
168fn decode_index_entry(bytes: &[u8]) -> Result<IndexEntry> {
169 let raw_id: [u8; 32] = bytes[..32].try_into().map_err(|_| {
170 crate::store::StoreError::InvalidObject("Invalid index id length".to_string())
171 })?;
172 let tagged_offset = u64::from_be_bytes(bytes[32..].try_into().map_err(|_| {
173 crate::store::StoreError::InvalidObject("Invalid offset length".to_string())
174 })?);
175 let id = if tagged_offset & STATE_ID_OFFSET_TAG != 0 {
176 PackObjectId::StateId(crate::object::StateId::from_bytes(raw_id))
177 } else if tagged_offset & ANNOTATED_TAG_OFFSET_TAG != 0 {
178 PackObjectId::AnnotatedTag(crate::object::ContentHash::from_bytes(raw_id))
179 } else {
180 PackObjectId::Hash(crate::object::ContentHash::from_bytes(raw_id))
181 };
182 Ok(IndexEntry {
183 id,
184 offset: tagged_offset & PACK_OFFSET_MASK,
185 })
186}
187
188impl PackIndex {
189 pub(super) fn entries(&self) -> Result<Vec<IndexEntry>> {
191 if let Some(encoded) = &self.encoded {
192 return (0..encoded.count)
193 .map(|index| encoded.entry(index))
194 .collect();
195 }
196 Ok(self.entries.clone())
197 }
198
199 pub fn ids(&self) -> Result<Vec<PackObjectId>> {
201 Ok(self.entries()?.into_iter().map(|entry| entry.id).collect())
202 }
203
204 pub(super) fn aliased_offsets(&self) -> Result<HashSet<u64>> {
205 let mut seen = HashSet::new();
206 let mut aliases = HashSet::new();
207 for entry in self.entries()? {
208 if !seen.insert(entry.offset) {
209 aliases.insert(entry.offset);
210 }
211 }
212 Ok(aliases)
213 }
214}
215
216impl Default for PackIndex {
217 fn default() -> Self {
218 Self::new()
219 }
220}
221
222fn verify_index_version(data: &[u8]) -> Result<()> {
223 if data.len() < 8 || &data[..4] != INDEX_MAGIC {
224 index_header().verify_layout(data)?;
225 unreachable!("invalid index header must have returned an error")
226 }
227 let version = u32::from_be_bytes(data[4..8].try_into().map_err(|_| {
228 crate::store::StoreError::InvalidObject("Index version field is truncated".to_string())
229 })?);
230 if version == INDEX_VERSION {
231 Ok(())
232 } else if version > INDEX_VERSION {
233 Err(crate::store::StoreError::InvalidObject(format!(
234 "pack index uses format version {version}, but this binary supports {INDEX_VERSION}; upgrade heddle"
235 )))
236 } else {
237 Err(crate::store::StoreError::InvalidObject(format!(
238 "pack index uses unsupported format version {version}; run `heddle migrate`"
239 )))
240 }
241}
242
243pub(super) fn index_header() -> VersionedHeader {
244 VersionedHeader {
245 magic: INDEX_MAGIC,
246 version: INDEX_VERSION,
247 checksum: HeaderChecksum::None,
248 too_short: "Index too short",
249 invalid_magic: "Invalid index magic",
250 unsupported_version: "Unsupported index version",
251 checksum_mismatch: "",
252 }
253}