1use crate::error::{Error, Result};
10
11pub const TILE_WIDTH: u64 = 256;
13pub const TILE_HEIGHT: u32 = 8;
15pub const HASH_SIZE: usize = 32;
17pub const MAX_ENTRY_LEN: usize = u16::MAX as usize;
19pub const TREE_STATE: &str = ".state/treeState";
21pub const TREE_STATE_LOCK: &str = ".state/treeState.lock";
23pub const PUBLISH_LOCK: &str = ".state/publish.lock";
25pub const CHECKPOINT: &str = "checkpoint";
27pub const MANIFEST: &str = "manifest";
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub struct Tile {
34 pub level: u8,
36 pub index: u64,
38 pub width: u16,
40}
41
42impl Tile {
43 pub fn new(level: u8, index: u64, width: u16) -> Result<Self> {
45 if width == 0 || u64::from(width) > TILE_WIDTH {
46 return Err(Error::OutOfRange(format!("tile width {width}")));
47 }
48 Ok(Self {
49 level,
50 index,
51 width,
52 })
53 }
54
55 #[must_use]
57 pub fn is_full(&self) -> bool {
58 u64::from(self.width) == TILE_WIDTH
59 }
60
61 #[must_use]
63 pub fn path(&self) -> String {
64 let mut path = format!("tile/{}/{}", self.level, path_elements(self.index));
65 if !self.is_full() {
66 path.push_str(&format!(".p/{}", self.width));
67 }
68 path
69 }
70
71 #[must_use]
74 pub fn parse(path: &str) -> Option<Self> {
75 let rest = path.strip_prefix("tile/")?;
76 let (level, rest) = rest.split_once('/')?;
77 if level.is_empty() || level.len() > 2 || !level.bytes().all(|b| b.is_ascii_digit()) {
78 return None;
79 }
80 if level.len() == 2 && level.starts_with('0') {
81 return None;
82 }
83 let level: u8 = level.parse().ok().filter(|l| *l < 64)?;
84 let (index, width) = parse_indexed(rest)?;
85 Self::new(level, index, width).ok()
86 }
87}
88
89#[must_use]
92pub fn path_elements(index: u64) -> String {
93 let digits = format!("{index:03}");
94 let mut groups: Vec<String> = Vec::new();
95 let bytes = digits.as_bytes();
96 let head = bytes.len() % 3;
97 let mut start = 0;
98 if head != 0 {
99 groups.push(format!("{:0>3}", &digits[..head]));
100 start = head;
101 }
102 while start < bytes.len() {
103 groups.push(digits[start..start + 3].to_string());
104 start += 3;
105 }
106 let last = groups.len() - 1;
107 groups
108 .iter()
109 .enumerate()
110 .map(|(i, g)| {
111 if i == last {
112 g.clone()
113 } else {
114 format!("x{g}")
115 }
116 })
117 .collect::<Vec<_>>()
118 .join("/")
119}
120
121#[must_use]
123pub fn parse_path_elements(text: &str) -> Option<u64> {
124 let elements: Vec<&str> = text.split('/').collect();
125 let last = elements.len() - 1;
126 let mut digits = String::new();
127 for (i, element) in elements.iter().enumerate() {
128 let body = if i == last {
129 *element
130 } else {
131 element.strip_prefix('x')?
132 };
133 if body.len() != 3 || !body.bytes().all(|b| b.is_ascii_digit()) {
134 return None;
135 }
136 digits.push_str(body);
137 }
138 let index: u64 = digits.parse().ok()?;
139 (path_elements(index) == text).then_some(index)
141}
142
143fn parse_indexed(rest: &str) -> Option<(u64, u16)> {
145 match rest.split_once(".p/") {
146 Some((index, width)) => {
147 if width.is_empty() || width.len() > 3 || !width.bytes().all(|b| b.is_ascii_digit()) {
148 return None;
149 }
150 if width.starts_with('0') {
151 return None;
152 }
153 let width: u16 = width.parse().ok()?;
154 if width == 0 || u64::from(width) >= TILE_WIDTH {
155 return None;
156 }
157 Some((parse_path_elements(index)?, width))
158 }
159 None => Some((parse_path_elements(rest)?, TILE_WIDTH as u16)),
160 }
161}
162
163#[must_use]
166pub fn entries_path(index: u64, width: u16) -> String {
167 let mut path = format!("tile/entries/{}", path_elements(index));
168 if u64::from(width) != TILE_WIDTH {
169 path.push_str(&format!(".p/{width}"));
170 }
171 path
172}
173
174#[must_use]
176pub fn parse_entries_path(path: &str) -> Option<(u64, u16)> {
177 parse_indexed(path.strip_prefix("tile/entries/")?)
178}
179
180#[must_use]
182pub fn index_path(index: u64) -> String {
183 format!("tile/index/{}", path_elements(index))
184}
185
186#[must_use]
188pub fn parse_index_path(path: &str) -> Option<u64> {
189 parse_path_elements(path.strip_prefix("tile/index/")?)
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub struct Layout {
195 pub size: u64,
197}
198
199impl Layout {
200 #[must_use]
202 pub fn new(size: u64) -> Self {
203 Self { size }
204 }
205
206 #[must_use]
208 pub fn hashes_at(&self, level: u8) -> u64 {
209 let shift = u32::from(level) * TILE_HEIGHT;
210 if shift >= u64::BITS {
211 0
212 } else {
213 self.size >> shift
214 }
215 }
216
217 #[must_use]
219 pub fn levels(&self) -> u8 {
220 let mut level = 0u8;
221 while self.hashes_at(level) > 0 {
222 level += 1;
223 }
224 level
225 }
226
227 #[must_use]
229 pub fn full_tiles_at(&self, level: u8) -> u64 {
230 self.hashes_at(level) / TILE_WIDTH
231 }
232
233 #[must_use]
236 pub fn width_at(&self, level: u8) -> u16 {
237 (self.hashes_at(level) % TILE_WIDTH) as u16
238 }
239
240 #[must_use]
242 pub fn partial_at(&self, level: u8) -> Option<Tile> {
243 let width = self.width_at(level);
244 (width > 0).then(|| Tile {
245 level,
246 index: self.full_tiles_at(level),
247 width,
248 })
249 }
250
251 #[must_use]
254 pub fn partial_tiles(&self) -> Vec<Tile> {
255 (0..self.levels())
256 .filter_map(|level| self.partial_at(level))
257 .collect()
258 }
259
260 #[must_use]
262 pub fn tiles(&self) -> Vec<Tile> {
263 let mut out = Vec::new();
264 for level in 0..self.levels() {
265 for index in 0..self.full_tiles_at(level) {
266 out.push(Tile {
267 level,
268 index,
269 width: TILE_WIDTH as u16,
270 });
271 }
272 out.extend(self.partial_at(level));
273 }
274 out
275 }
276
277 #[must_use]
280 pub fn tile(&self, level: u8, index: u64) -> Option<Tile> {
281 let full = self.full_tiles_at(level);
282 if index < full {
283 Some(Tile {
284 level,
285 index,
286 width: TILE_WIDTH as u16,
287 })
288 } else if index == full {
289 self.partial_at(level)
290 } else {
291 None
292 }
293 }
294
295 #[must_use]
297 pub fn full_bundles(&self) -> u64 {
298 self.full_tiles_at(0)
299 }
300
301 #[must_use]
303 pub fn partial_bundle_width(&self) -> u16 {
304 self.width_at(0)
305 }
306
307 #[must_use]
309 pub fn bundle_of(entry: u64) -> (u64, u16) {
310 (entry / TILE_WIDTH, (entry % TILE_WIDTH) as u16)
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn the_spec_path_example() {
320 assert_eq!(path_elements(1_234_067), "x001/x234/067");
321 assert_eq!(parse_path_elements("x001/x234/067"), Some(1_234_067));
322 assert_eq!(path_elements(0), "000");
323 assert_eq!(path_elements(7), "007");
324 assert_eq!(path_elements(1000), "x001/000");
325 assert_eq!(parse_path_elements("x000/007"), None);
326 assert_eq!(parse_path_elements("7"), None);
327 }
328
329 #[test]
330 fn tile_paths_round_trip() {
331 let full = Tile::new(1, 300, 256).unwrap();
332 assert_eq!(full.path(), "tile/1/300");
333 let partial = Tile::new(0, 273, 112).unwrap();
334 assert_eq!(partial.path(), "tile/0/273.p/112");
335 assert_eq!(
336 Tile::new(0, 1_234_067, 5).unwrap().path(),
337 "tile/0/x001/x234/067.p/5"
338 );
339 for tile in [full, partial] {
340 assert_eq!(Tile::parse(&tile.path()), Some(tile));
341 }
342 assert_eq!(Tile::parse("tile/entries/000"), None);
343 assert_eq!(Tile::parse("tile/0/000.p/256"), None);
344 assert_eq!(Tile::parse("tile/0/000.p/0"), None);
345 assert_eq!(Tile::parse("tile/00/000"), None);
346 assert_eq!(entries_path(5, 256), "tile/entries/005");
347 assert_eq!(entries_path(5, 12), "tile/entries/005.p/12");
348 assert_eq!(parse_entries_path("tile/entries/005.p/12"), Some((5, 12)));
349 assert_eq!(index_path(1000), "tile/index/x001/000");
350 assert_eq!(parse_index_path("tile/index/x001/000"), Some(1000));
351 }
352
353 #[test]
354 fn widths_follow_the_size() {
355 let layout = Layout::new(256);
356 assert_eq!(layout.levels(), 2);
357 assert_eq!(layout.full_tiles_at(0), 1);
358 assert_eq!(layout.width_at(0), 0);
359 assert_eq!(layout.width_at(1), 1);
360 assert_eq!(Layout::new(0).levels(), 0);
361 assert!(Layout::new(0).tiles().is_empty());
362 assert_eq!(Layout::new(1).tiles(), vec![Tile::new(0, 0, 1).unwrap()]);
363 assert_eq!(Layout::bundle_of(70_000), (273, 112));
364 }
365}