1use std::collections::BTreeMap;
36use std::fs::{self, File, OpenOptions};
37use std::io::{self, Read, Seek, SeekFrom, Write};
38use std::path::{Path, PathBuf};
39use std::sync::Mutex;
40use std::time::{Duration, Instant};
41
42use sha2::{Digest, Sha256};
43
44use crate::store::TileStore;
45
46pub const PACK_MAGIC: &[u8] = b"rfc9162-tiles/pack/v2\n";
49pub const PACK_MAGIC_V1: &[u8] = b"rfc9162-tiles/pack/v1\n";
51
52const KIND_PUT: u8 = 1;
53const KIND_DELETE: u8 = 2;
54const KIND_RENAME: u8 = 3;
55const KIND_INDEX: u8 = 4;
57
58const CODEC_RAW: u8 = 0;
59const CODEC_DEFLATE: u8 = 1;
60
61const COMPRESS_FROM: usize = 128;
64const DEFLATE_LEVEL: u8 = 6;
65
66const HEAD_LEN: usize = 1 + 1 + 2 + 4 + 4;
68const DIGEST_LEN: usize = 32;
69
70const INDEX_FIXED: usize = 8 + 4;
72const INDEX_PER_PATH: usize = 2 + 8 + 8 + 4 + 4 + 1;
75
76fn index_len(paths: usize, names: usize) -> u64 {
79 (HEAD_LEN + INDEX_FIXED + DIGEST_LEN + paths * INDEX_PER_PATH + names) as u64
80}
81
82#[derive(Debug, Clone, Copy)]
84struct Slot {
85 start: u64,
88 offset: u64,
90 stored: u32,
91 raw: u32,
92 codec: u8,
93}
94
95struct Inner {
96 file: File,
97 valid_len: u64,
100 live: BTreeMap<String, Slot>,
101 live_bytes: u64,
104 #[cfg(unix)]
105 inode: u64,
106}
107
108pub struct PackStore {
110 path: PathBuf,
111 writer: bool,
112 shared: Option<Duration>,
115 inner: Mutex<Inner>,
116}
117
118pub const SHARED_LOCK_WAIT: Duration = Duration::from_secs(30);
120
121struct ChangeLock(PathBuf);
123
124impl Drop for ChangeLock {
125 fn drop(&mut self) {
126 let _ = fs::remove_file(&self.0);
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct Compacted {
133 pub before: u64,
135 pub after: u64,
137 pub paths: usize,
139}
140
141impl PackStore {
142 pub fn open(path: impl Into<PathBuf>) -> io::Result<Self> {
146 let path = path.into();
147 if let Some(parent) = path.parent() {
148 if !parent.as_os_str().is_empty() {
149 fs::create_dir_all(parent)?;
150 }
151 }
152 take_lock(&lock_path(&path))?;
153 let store = match Self::open_with(path.clone(), true) {
154 Ok(store) => store,
155 Err(e) => {
156 let _ = fs::remove_file(lock_path(&path));
157 return Err(e);
158 }
159 };
160 {
163 let inner = store.inner.lock().unwrap();
164 let valid = inner.valid_len;
165 if inner.file.metadata()?.len() != valid {
166 inner.file.set_len(valid)?;
167 inner.file.sync_all()?;
168 }
169 }
170 Ok(store)
171 }
172
173 pub fn open_shared(path: impl Into<PathBuf>) -> io::Result<Self> {
178 Self::open_shared_waiting(path, SHARED_LOCK_WAIT)
179 }
180
181 pub fn open_shared_waiting(path: impl Into<PathBuf>, wait: Duration) -> io::Result<Self> {
183 let path = path.into();
184 if let Some(parent) = path.parent() {
185 if !parent.as_os_str().is_empty() {
186 fs::create_dir_all(parent)?;
187 }
188 }
189 let store = {
192 let _lock = acquire(&lock_path(&path), wait)?;
193 let mut store = Self::open_with(path, true)?;
194 store.shared = Some(wait);
195 store
196 };
197 Ok(store)
198 }
199
200 pub fn open_read(path: impl Into<PathBuf>) -> io::Result<Self> {
203 Self::open_with(path.into(), false)
204 }
205
206 pub fn break_lock(path: impl AsRef<Path>) -> io::Result<bool> {
209 match fs::remove_file(lock_path(path.as_ref())) {
210 Ok(()) => Ok(true),
211 Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
212 Err(e) => Err(e),
213 }
214 }
215
216 #[must_use]
218 pub fn path(&self) -> &Path {
219 &self.path
220 }
221
222 pub fn len(&self) -> io::Result<u64> {
224 Ok(fs::metadata(&self.path)?.len())
225 }
226
227 pub fn is_empty(&self) -> io::Result<bool> {
229 let mut inner = self.inner.lock().unwrap();
230 self.refresh(&mut inner)?;
231 Ok(inner.live.is_empty())
232 }
233
234 pub fn live_bytes(&self) -> io::Result<u64> {
237 let mut inner = self.inner.lock().unwrap();
238 self.refresh(&mut inner)?;
239 Ok(inner.live_bytes)
240 }
241
242 pub fn compact(&self) -> io::Result<Compacted> {
246 self.need_writer()?;
247 let mut inner = self.inner.lock().unwrap();
248 let _lock = self.begin(&mut inner)?;
249 self.refresh(&mut inner)?;
250 let before = inner.valid_len;
251 let temp = self.path.with_extension("compacting");
252 let mut out = File::create(&temp)?;
253 out.write_all(PACK_MAGIC)?;
254 let current: Vec<(String, Slot)> = inner
255 .live
256 .iter()
257 .map(|(path, slot)| (path.clone(), *slot))
258 .collect();
259 let names: usize = current.iter().map(|(p, _)| p.len()).sum();
260 let first = PACK_MAGIC.len() as u64 + index_len(current.len(), names);
261 let mut offset = first;
262 let mut live = BTreeMap::new();
263 let mut records = Vec::with_capacity(current.len());
264 for (path, slot) in ¤t {
265 let stored = read_record(&mut inner.file, slot)?;
266 let record = encode_stored(KIND_PUT, slot.codec, path, &stored, slot.raw)?;
267 let body_at = offset + (HEAD_LEN + path.len()) as u64;
268 live.insert(
269 path.clone(),
270 Slot {
271 start: offset,
272 offset: body_at,
273 stored: slot.stored,
274 raw: slot.raw,
275 codec: slot.codec,
276 },
277 );
278 offset += record.len() as u64;
279 records.push(record);
280 }
281 let index = encode_index(offset, &live)?;
282 debug_assert_eq!(PACK_MAGIC.len() as u64 + index.len() as u64, first);
283 out.write_all(&index)?;
284 for record in &records {
285 out.write_all(record)?;
286 }
287 let live_bytes = offset;
288 out.sync_all()?;
289 drop(out);
290 fs::rename(&temp, &self.path)?;
291 sync_parent(&self.path)?;
292 let file = OpenOptions::new().read(true).write(true).open(&self.path)?;
293 #[cfg(unix)]
294 let inode = {
295 use std::os::unix::fs::MetadataExt;
296 file.metadata()?.ino()
297 };
298 *inner = Inner {
299 file,
300 valid_len: offset,
301 live,
302 live_bytes,
303 #[cfg(unix)]
304 inode,
305 };
306 Ok(Compacted {
307 before,
308 after: offset,
309 paths: inner.live.len(),
310 })
311 }
312
313 fn open_with(path: PathBuf, writer: bool) -> io::Result<Self> {
314 let inner = load(&path, writer)?;
315 Ok(Self {
316 path,
317 writer,
318 shared: None,
319 inner: Mutex::new(inner),
320 })
321 }
322
323 fn need_writer(&self) -> io::Result<()> {
324 if self.writer {
325 Ok(())
326 } else {
327 Err(io::Error::new(
328 io::ErrorKind::PermissionDenied,
329 format!("{} is open for reading", self.path.display()),
330 ))
331 }
332 }
333
334 fn begin(&self, inner: &mut Inner) -> io::Result<Option<ChangeLock>> {
338 let Some(wait) = self.shared else {
339 return Ok(None);
340 };
341 let lock = acquire(&lock_path(&self.path), wait)?;
342 self.refresh(inner)?;
343 if inner.file.metadata()?.len() != inner.valid_len {
344 inner.file.set_len(inner.valid_len)?;
345 inner.file.sync_all()?;
346 }
347 Ok(Some(lock))
348 }
349
350 fn refresh(&self, inner: &mut Inner) -> io::Result<()> {
353 if self.writer && self.shared.is_none() {
354 return Ok(());
355 }
356 let meta = fs::metadata(&self.path)?;
357 #[cfg(unix)]
358 let replaced = {
359 use std::os::unix::fs::MetadataExt;
360 meta.ino() != inner.inode
361 };
362 #[cfg(not(unix))]
363 let replaced = meta.len() < inner.valid_len;
364 if replaced {
365 *inner = load(&self.path, self.writer)?;
366 return Ok(());
367 }
368 if meta.len() > inner.valid_len {
369 scan(inner)?;
370 }
371 Ok(())
372 }
373
374 fn append(&self, inner: &mut Inner, kind: u8, path: &str, body: &[u8]) -> io::Result<Slot> {
375 self.need_writer()?;
376 let (codec, stored) = if kind == KIND_PUT && body.len() >= COMPRESS_FROM {
377 let packed = miniz_oxide::deflate::compress_to_vec(body, DEFLATE_LEVEL);
378 if packed.len() * 10 < body.len() * 9 {
379 (CODEC_DEFLATE, packed)
380 } else {
381 (CODEC_RAW, body.to_vec())
382 }
383 } else {
384 (CODEC_RAW, body.to_vec())
385 };
386 let raw = u32::try_from(body.len())
387 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "body over 4 GiB"))?;
388 let record = encode_stored(kind, codec, path, &stored, raw)?;
389 let at = inner.valid_len;
390 inner.file.seek(SeekFrom::Start(at))?;
391 inner.file.write_all(&record)?;
392 inner.file.sync_data()?;
393 inner.valid_len = at + record.len() as u64;
394 Ok(Slot {
395 start: at,
396 offset: at + (HEAD_LEN + path.len()) as u64,
397 stored: stored.len() as u32,
398 raw,
399 codec,
400 })
401 }
402}
403
404fn load(path: &Path, writer: bool) -> io::Result<Inner> {
406 let file = OpenOptions::new()
407 .read(true)
408 .write(writer)
409 .create(writer)
410 .open(path)?;
411 #[cfg(unix)]
412 let inode = {
413 use std::os::unix::fs::MetadataExt;
414 file.metadata()?.ino()
415 };
416 let mut inner = Inner {
417 file,
418 valid_len: 0,
419 live: BTreeMap::new(),
420 live_bytes: PACK_MAGIC.len() as u64 + index_len(0, 0),
421 #[cfg(unix)]
422 inode,
423 };
424 let len = inner.file.metadata()?.len();
425 if len == 0 {
426 if !writer {
427 return Err(io::Error::new(
428 io::ErrorKind::InvalidData,
429 format!("{} is empty, not a pack", path.display()),
430 ));
431 }
432 inner.file.write_all(PACK_MAGIC)?;
433 inner.file.sync_all()?;
434 inner.valid_len = PACK_MAGIC.len() as u64;
435 } else {
436 let mut magic = vec![0u8; PACK_MAGIC.len()];
437 inner.file.seek(SeekFrom::Start(0))?;
438 let v2 = match inner.file.read_exact(&mut magic) {
439 Ok(()) if magic == PACK_MAGIC => true,
440 Ok(()) if magic == PACK_MAGIC_V1 => false,
441 _ => {
442 return Err(io::Error::new(
443 io::ErrorKind::InvalidData,
444 format!("{} is not a pack", path.display()),
445 ))
446 }
447 };
448 inner.valid_len = PACK_MAGIC.len() as u64;
449 if v2 {
450 read_index(&mut inner, len)?;
451 }
452 if v2 && inner.valid_len == PACK_MAGIC.len() as u64 {
453 skip_unusable_index(&mut inner, len)?;
454 }
455 scan(&mut inner)?;
456 }
457 Ok(inner)
458}
459
460impl Drop for PackStore {
461 fn drop(&mut self) {
462 if self.writer && self.shared.is_none() {
463 let _ = fs::remove_file(lock_path(&self.path));
464 }
465 }
466}
467
468impl TileStore for PackStore {
469 fn get(&self, path: &str) -> io::Result<Option<Vec<u8>>> {
470 let mut inner = self.inner.lock().unwrap();
471 self.refresh(&mut inner)?;
472 let Some(slot) = inner.live.get(path).copied() else {
473 return Ok(None);
474 };
475 let stored = read_record(&mut inner.file, &slot)?;
476 Ok(Some(unpack(&slot, stored)?))
477 }
478
479 fn put(&self, path: &str, bytes: &[u8]) -> io::Result<()> {
480 check_path(path)?;
481 self.need_writer()?;
482 let mut inner = self.inner.lock().unwrap();
483 let _lock = self.begin(&mut inner)?;
484 let slot = self.append(&mut inner, KIND_PUT, path, bytes)?;
485 record_live(&mut inner, path, slot);
486 Ok(())
487 }
488
489 fn create_new(&self, path: &str, bytes: &[u8]) -> io::Result<bool> {
490 check_path(path)?;
491 self.need_writer()?;
492 let mut inner = self.inner.lock().unwrap();
493 let _lock = self.begin(&mut inner)?;
494 if inner.live.contains_key(path) {
495 return Ok(false);
496 }
497 let slot = self.append(&mut inner, KIND_PUT, path, bytes)?;
498 record_live(&mut inner, path, slot);
499 Ok(true)
500 }
501
502 fn delete(&self, path: &str) -> io::Result<bool> {
503 self.need_writer()?;
504 let mut inner = self.inner.lock().unwrap();
505 let _lock = self.begin(&mut inner)?;
506 if !inner.live.contains_key(path) {
507 return Ok(false);
508 }
509 self.append(&mut inner, KIND_DELETE, path, &[])?;
510 drop_live(&mut inner, path);
511 Ok(true)
512 }
513
514 fn rename(&self, from: &str, to: &str) -> io::Result<()> {
515 check_path(to)?;
516 self.need_writer()?;
517 let mut inner = self.inner.lock().unwrap();
518 let _lock = self.begin(&mut inner)?;
519 let Some(slot) = inner.live.get(from).copied() else {
520 return Err(io::Error::new(io::ErrorKind::NotFound, from.to_string()));
521 };
522 self.append(&mut inner, KIND_RENAME, from, to.as_bytes())?;
523 drop_live(&mut inner, from);
524 record_live(&mut inner, to, slot);
525 Ok(())
526 }
527
528 fn list(&self, prefix: &str) -> io::Result<Vec<String>> {
529 let mut inner = self.inner.lock().unwrap();
530 self.refresh(&mut inner)?;
531 Ok(inner
532 .live
533 .keys()
534 .filter(|k| under(k, prefix))
535 .cloned()
536 .collect())
537 }
538
539 fn exists(&self, path: &str) -> io::Result<bool> {
540 let mut inner = self.inner.lock().unwrap();
541 self.refresh(&mut inner)?;
542 Ok(inner.live.contains_key(path))
543 }
544}
545
546fn under(path: &str, prefix: &str) -> bool {
547 prefix.is_empty()
548 || path
549 .strip_prefix(prefix)
550 .is_some_and(|r| r.starts_with('/'))
551}
552
553fn check_path(path: &str) -> io::Result<()> {
554 if path.is_empty()
555 || path.len() > u16::MAX as usize
556 || path.starts_with('/')
557 || path
558 .split('/')
559 .any(|c| c.is_empty() || c == "." || c == "..")
560 {
561 return Err(io::Error::new(
562 io::ErrorKind::InvalidInput,
563 format!("store path {path:?} is not a relative path of plain components"),
564 ));
565 }
566 Ok(())
567}
568
569fn lock_path(pack: &Path) -> PathBuf {
570 let mut name = pack.as_os_str().to_owned();
571 name.push(".lock");
572 PathBuf::from(name)
573}
574
575fn acquire(lock: &Path, wait: Duration) -> io::Result<ChangeLock> {
578 let start = Instant::now();
579 let mut pause = Duration::from_millis(1);
580 loop {
581 match take_lock(lock) {
582 Ok(()) => return Ok(ChangeLock(lock.to_path_buf())),
583 Err(e) if e.kind() == io::ErrorKind::WouldBlock && start.elapsed() < wait => {
584 std::thread::sleep(pause.min(wait.saturating_sub(start.elapsed())));
585 pause = (pause * 2).min(Duration::from_millis(50));
586 }
587 Err(e) => return Err(e),
588 }
589 }
590}
591
592fn take_lock(lock: &Path) -> io::Result<()> {
593 match OpenOptions::new().write(true).create_new(true).open(lock) {
594 Ok(mut file) => {
595 let _ = writeln!(file, "{}", std::process::id());
596 Ok(())
597 }
598 Err(e) if e.kind() == io::ErrorKind::AlreadyExists => Err(io::Error::new(
599 io::ErrorKind::WouldBlock,
600 format!("{} is held by another writer", lock.display()),
601 )),
602 Err(e) => Err(e),
603 }
604}
605
606fn sync_parent(path: &Path) -> io::Result<()> {
607 if let Some(parent) = path.parent() {
608 if !parent.as_os_str().is_empty() {
609 File::open(parent)?.sync_all()?;
610 }
611 }
612 Ok(())
613}
614
615fn encode_stored(kind: u8, codec: u8, path: &str, stored: &[u8], raw: u32) -> io::Result<Vec<u8>> {
617 let path_len = u16::try_from(path.len())
618 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path over 64 KiB"))?;
619 let stored_len = u32::try_from(stored.len())
620 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "body over 4 GiB"))?;
621 let mut out = Vec::with_capacity(HEAD_LEN + path.len() + stored.len() + DIGEST_LEN);
622 out.push(kind);
623 out.push(codec);
624 out.extend_from_slice(&path_len.to_be_bytes());
625 out.extend_from_slice(&stored_len.to_be_bytes());
626 out.extend_from_slice(&raw.to_be_bytes());
627 out.extend_from_slice(path.as_bytes());
628 out.extend_from_slice(stored);
629 let digest = Sha256::digest(&out);
630 out.extend_from_slice(&digest);
631 Ok(out)
632}
633
634fn read_record(file: &mut File, slot: &Slot) -> io::Result<Vec<u8>> {
638 let corrupt = || io::Error::new(io::ErrorKind::InvalidData, "pack record fails its digest");
639 let body_at = usize::try_from(slot.offset - slot.start).map_err(|_| corrupt())?;
640 let mut record = vec![0u8; body_at + slot.stored as usize + DIGEST_LEN];
641 file.seek(SeekFrom::Start(slot.start))?;
642 file.read_exact(&mut record)?;
643 let (framed, digest) = record.split_at(record.len() - DIGEST_LEN);
644 let path_len = u16::from_be_bytes([framed[2], framed[3]]) as usize;
645 let stored_len = u32::from_be_bytes([framed[4], framed[5], framed[6], framed[7]]);
646 if Sha256::digest(framed).as_slice() != digest
647 || framed[0] != KIND_PUT
648 || framed[1] != slot.codec
649 || HEAD_LEN + path_len != body_at
650 || stored_len != slot.stored
651 {
652 return Err(corrupt());
653 }
654 Ok(framed[body_at..].to_vec())
655}
656
657fn unpack(slot: &Slot, stored: Vec<u8>) -> io::Result<Vec<u8>> {
658 match slot.codec {
659 CODEC_RAW => Ok(stored),
660 CODEC_DEFLATE => {
661 let raw = miniz_oxide::inflate::decompress_to_vec(&stored).map_err(|e| {
662 io::Error::new(io::ErrorKind::InvalidData, format!("pack record: {e:?}"))
663 })?;
664 if raw.len() != slot.raw as usize {
665 return Err(io::Error::new(
666 io::ErrorKind::InvalidData,
667 "pack record inflates to the wrong length",
668 ));
669 }
670 Ok(raw)
671 }
672 other => Err(io::Error::new(
673 io::ErrorKind::InvalidData,
674 format!("pack record codec {other}"),
675 )),
676 }
677}
678
679fn live_cost(path: &str, slot: &Slot) -> u64 {
681 (HEAD_LEN + path.len() + DIGEST_LEN + INDEX_PER_PATH + path.len()) as u64
682 + u64::from(slot.stored)
683}
684
685fn record_live(inner: &mut Inner, path: &str, slot: Slot) {
686 if let Some(old) = inner.live.insert(path.to_string(), slot) {
687 inner.live_bytes -= live_cost(path, &old);
688 }
689 inner.live_bytes += live_cost(path, &slot);
690}
691
692fn drop_live(inner: &mut Inner, path: &str) {
693 if let Some(old) = inner.live.remove(path) {
694 inner.live_bytes -= live_cost(path, &old);
695 }
696}
697
698fn encode_index(covered: u64, live: &BTreeMap<String, Slot>) -> io::Result<Vec<u8>> {
701 let mut body = Vec::with_capacity(INDEX_FIXED + live.len() * INDEX_PER_PATH);
702 body.extend_from_slice(&covered.to_be_bytes());
703 let n = u32::try_from(live.len())
704 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "too many paths"))?;
705 body.extend_from_slice(&n.to_be_bytes());
706 for (path, slot) in live {
707 let len = u16::try_from(path.len())
708 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path over 64 KiB"))?;
709 body.extend_from_slice(&len.to_be_bytes());
710 body.extend_from_slice(path.as_bytes());
711 body.extend_from_slice(&slot.start.to_be_bytes());
712 body.extend_from_slice(&slot.offset.to_be_bytes());
713 body.extend_from_slice(&slot.stored.to_be_bytes());
714 body.extend_from_slice(&slot.raw.to_be_bytes());
715 body.push(slot.codec);
716 }
717 let raw = u32::try_from(body.len())
718 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "index over 4 GiB"))?;
719 encode_stored(KIND_INDEX, CODEC_RAW, "", &body, raw)
720}
721
722fn read_index(inner: &mut Inner, file_len: u64) -> io::Result<()> {
728 let at = inner.valid_len;
729 let mut head = [0u8; HEAD_LEN];
730 inner.file.seek(SeekFrom::Start(at))?;
731 if inner.file.read_exact(&mut head).is_err() || head[0] != KIND_INDEX || head[1] != CODEC_RAW {
732 return Ok(());
733 }
734 let path_len = u16::from_be_bytes([head[2], head[3]]) as usize;
735 let stored_len = u32::from_be_bytes([head[4], head[5], head[6], head[7]]) as usize;
736 let end = at + (HEAD_LEN + path_len + stored_len + DIGEST_LEN) as u64;
737 if path_len != 0 || end > file_len {
738 return Ok(());
739 }
740 let mut record = head.to_vec();
741 record.resize(HEAD_LEN + stored_len + DIGEST_LEN, 0);
742 inner.file.read_exact(&mut record[HEAD_LEN..])?;
743 let (framed, digest) = record.split_at(HEAD_LEN + stored_len);
744 if Sha256::digest(framed).as_slice() != digest {
745 return Ok(());
746 }
747 let body = &framed[HEAD_LEN..];
748 let Some(table) = parse_index(body, end) else {
749 return Ok(());
750 };
751 let (covered, slots) = table;
752 if covered > file_len {
753 return Ok(());
754 }
755 let mut live = BTreeMap::new();
756 let mut live_bytes = PACK_MAGIC.len() as u64 + index_len(0, 0);
757 for (path, slot) in slots {
758 live_bytes += live_cost(&path, &slot);
759 live.insert(path, slot);
760 }
761 inner.live = live;
762 inner.live_bytes = live_bytes;
763 inner.valid_len = covered;
764 Ok(())
765}
766
767fn skip_unusable_index(inner: &mut Inner, file_len: u64) -> io::Result<()> {
772 let at = inner.valid_len;
773 let mut head = [0u8; HEAD_LEN];
774 inner.file.seek(SeekFrom::Start(at))?;
775 if inner.file.read_exact(&mut head).is_err() || head[0] != KIND_INDEX {
776 return Ok(());
777 }
778 let path_len = u16::from_be_bytes([head[2], head[3]]) as u64;
779 let stored_len = u32::from_be_bytes([head[4], head[5], head[6], head[7]]) as u64;
780 let end = at + HEAD_LEN as u64 + path_len + stored_len + DIGEST_LEN as u64;
781 if end <= file_len {
782 inner.valid_len = end;
783 }
784 Ok(())
785}
786
787fn parse_index(body: &[u8], first: u64) -> Option<(u64, Vec<(String, Slot)>)> {
791 let covered = u64::from_be_bytes(body.get(..8)?.try_into().ok()?);
792 let n = u32::from_be_bytes(body.get(8..12)?.try_into().ok()?) as usize;
793 if covered < first || n > body.len() / INDEX_PER_PATH {
794 return None;
795 }
796 let mut at = INDEX_FIXED;
797 let mut slots = Vec::with_capacity(n);
798 for _ in 0..n {
799 let len = u16::from_be_bytes(body.get(at..at + 2)?.try_into().ok()?) as usize;
800 at += 2;
801 let path = std::str::from_utf8(body.get(at..at + len)?)
802 .ok()?
803 .to_string();
804 at += len;
805 let row = body.get(at..at + INDEX_PER_PATH - 2)?;
806 at += INDEX_PER_PATH - 2;
807 let slot = Slot {
808 start: u64::from_be_bytes(row[..8].try_into().ok()?),
809 offset: u64::from_be_bytes(row[8..16].try_into().ok()?),
810 stored: u32::from_be_bytes(row[16..20].try_into().ok()?),
811 raw: u32::from_be_bytes(row[20..24].try_into().ok()?),
812 codec: row[24],
813 };
814 let stop = slot
815 .offset
816 .checked_add(u64::from(slot.stored) + DIGEST_LEN as u64)?;
817 if slot.start < first || slot.offset < slot.start + HEAD_LEN as u64 || stop > covered {
818 return None;
819 }
820 slots.push((path, slot));
821 }
822 (at == body.len()).then_some((covered, slots))
823}
824
825fn scan(inner: &mut Inner) -> io::Result<()> {
830 let mut at = inner.valid_len;
831 inner.file.seek(SeekFrom::Start(at))?;
832 let mut reader = io::BufReader::new(inner.file.try_clone()?);
833 let mut head = [0u8; HEAD_LEN];
834 let mut record = Vec::new();
835 let mut digest = [0u8; DIGEST_LEN];
836 loop {
837 if reader.read_exact(&mut head).is_err() {
838 break;
839 }
840 let kind = head[0];
841 let codec = head[1];
842 let path_len = u16::from_be_bytes([head[2], head[3]]) as usize;
843 let stored_len = u32::from_be_bytes([head[4], head[5], head[6], head[7]]) as usize;
844 let raw = u32::from_be_bytes([head[8], head[9], head[10], head[11]]);
845 record.clear();
846 record.extend_from_slice(&head);
847 record.resize(HEAD_LEN + path_len + stored_len, 0);
848 if reader.read_exact(&mut record[HEAD_LEN..]).is_err()
849 || reader.read_exact(&mut digest).is_err()
850 {
851 break;
852 }
853 if Sha256::digest(&record).as_slice() != digest {
854 break;
855 }
856 let Ok(path) = std::str::from_utf8(&record[HEAD_LEN..HEAD_LEN + path_len]) else {
857 break;
858 };
859 let slot = Slot {
860 start: at,
861 offset: at + (HEAD_LEN + path_len) as u64,
862 stored: stored_len as u32,
863 raw,
864 codec,
865 };
866 match kind {
867 KIND_PUT => record_live(inner, path, slot),
868 KIND_DELETE => drop_live(inner, path),
869 KIND_RENAME => {
870 let Ok(to) = std::str::from_utf8(&record[HEAD_LEN + path_len..]) else {
871 break;
872 };
873 let to = to.to_string();
874 if let Some(moved) = inner.live.remove(path) {
875 inner.live_bytes -= live_cost(path, &moved);
876 record_live(inner, &to, moved);
877 }
878 }
879 KIND_INDEX => {}
882 _ => break,
883 }
884 at += (HEAD_LEN + path_len + stored_len + DIGEST_LEN) as u64;
885 }
886 inner.valid_len = at;
887 Ok(())
888}
889
890#[cfg(test)]
891mod tests {
892 use super::*;
893
894 fn body(i: usize) -> Vec<u8> {
895 format!(
896 "{{\"module\":\"PETSc/3.24.{i}-foss-2026.1\",\"pad\":\"{}\"}}",
897 "x".repeat(200)
898 )
899 .into_bytes()
900 }
901
902 fn fill(store: &PackStore, n: usize) {
903 for i in 0..n {
904 store
905 .put(&format!("tile/entries/{i:03}"), &body(i))
906 .unwrap();
907 }
908 store.put("tile/entries/000", b"rewritten").unwrap();
909 store.delete("tile/entries/001").unwrap();
910 store.rename("tile/entries/002", "moved/two").unwrap();
911 }
912
913 fn check(store: &PackStore, n: usize) {
914 assert_eq!(
915 store.get("tile/entries/000").unwrap().as_deref(),
916 Some(&b"rewritten"[..])
917 );
918 assert_eq!(store.get("tile/entries/001").unwrap(), None);
919 assert_eq!(store.get("tile/entries/002").unwrap(), None);
920 assert_eq!(store.get("moved/two").unwrap(), Some(body(2)));
921 for i in 3..n {
922 assert_eq!(
923 store.get(&format!("tile/entries/{i:03}")).unwrap(),
924 Some(body(i))
925 );
926 }
927 }
928
929 #[test]
930 fn a_compacted_pack_opens_through_its_index_and_takes_later_records() {
931 let dir = tempfile::tempdir().unwrap();
932 let pack = dir.path().join("pack");
933 let store = PackStore::open(&pack).unwrap();
934 fill(&store, 50);
935 let live = store.live_bytes().unwrap();
936 let done = store.compact().unwrap();
937 assert_eq!(done.after, live);
938 store.put("after/compaction", b"tail").unwrap();
939 store.rename("tile/entries/003", "moved/three").unwrap();
940 drop(store);
941 let reader = PackStore::open_read(&pack).unwrap();
942 {
943 let inner = reader.inner.lock().unwrap();
944 assert_eq!(inner.valid_len, std::fs::metadata(&pack).unwrap().len());
945 }
946 assert_eq!(
947 reader.get("after/compaction").unwrap().as_deref(),
948 Some(&b"tail"[..])
949 );
950 assert_eq!(reader.get("moved/three").unwrap(), Some(body(3)));
951 assert_eq!(reader.get("tile/entries/003").unwrap(), None);
952 assert_eq!(reader.get("moved/two").unwrap(), Some(body(2)));
953 assert_eq!(reader.get("tile/entries/049").unwrap(), Some(body(49)));
954 let writer = PackStore::open(&pack).unwrap();
955 let live = writer.live_bytes().unwrap();
956 assert_eq!(writer.compact().unwrap().after, live);
957 assert_eq!(writer.get("moved/three").unwrap(), Some(body(3)));
958 }
959
960 #[test]
961 fn the_index_is_what_open_reads() {
962 let dir = tempfile::tempdir().unwrap();
963 let pack = dir.path().join("pack");
964 let store = PackStore::open(&pack).unwrap();
965 fill(&store, 20);
966 store.compact().unwrap();
967 drop(store);
968 let mut file = OpenOptions::new().read(true).open(&pack).unwrap();
969 let mut inner = Inner {
970 file: file.try_clone().unwrap(),
971 valid_len: PACK_MAGIC.len() as u64,
972 live: BTreeMap::new(),
973 live_bytes: 0,
974 #[cfg(unix)]
975 inode: 0,
976 };
977 let len = file.seek(SeekFrom::End(0)).unwrap();
978 read_index(&mut inner, len).unwrap();
979 assert_eq!(inner.valid_len, len);
980 assert_eq!(inner.live.len(), 19);
981 }
982
983 #[test]
984 fn a_damaged_index_falls_back_to_reading_every_record() {
985 let dir = tempfile::tempdir().unwrap();
986 let pack = dir.path().join("pack");
987 let store = PackStore::open(&pack).unwrap();
988 fill(&store, 30);
989 store.compact().unwrap();
990 drop(store);
991 let mut bytes = std::fs::read(&pack).unwrap();
992 bytes[PACK_MAGIC.len() + HEAD_LEN + 3] ^= 0xff;
994 std::fs::write(&pack, &bytes).unwrap();
995 let reader = PackStore::open_read(&pack).unwrap();
996 check(&reader, 30);
997 let writer = PackStore::open(&pack).unwrap();
998 assert_eq!(std::fs::metadata(&pack).unwrap().len(), bytes.len() as u64);
999 check(&writer, 30);
1000 }
1001
1002 #[test]
1003 fn a_damaged_record_behind_the_index_is_refused_when_read() {
1004 let dir = tempfile::tempdir().unwrap();
1005 let pack = dir.path().join("pack");
1006 let store = PackStore::open(&pack).unwrap();
1007 fill(&store, 10);
1008 store.compact().unwrap();
1009 let slot = store.inner.lock().unwrap().live["tile/entries/005"];
1010 drop(store);
1011 let mut bytes = std::fs::read(&pack).unwrap();
1012 bytes[slot.offset as usize] ^= 0xff;
1013 std::fs::write(&pack, &bytes).unwrap();
1014 let reader = PackStore::open_read(&pack).unwrap();
1015 let err = reader.get("tile/entries/005").unwrap_err();
1016 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
1017 assert_eq!(reader.get("tile/entries/006").unwrap(), Some(body(6)));
1018 }
1019
1020 #[test]
1021 fn a_v1_pack_opens_and_becomes_v2_at_compaction() {
1022 let dir = tempfile::tempdir().unwrap();
1023 let pack = dir.path().join("pack");
1024 let mut bytes = PACK_MAGIC_V1.to_vec();
1025 for i in 0..5 {
1026 let raw = body(i);
1027 bytes.extend(
1028 encode_stored(
1029 KIND_PUT,
1030 CODEC_RAW,
1031 &format!("p/{i}"),
1032 &raw,
1033 raw.len() as u32,
1034 )
1035 .unwrap(),
1036 );
1037 }
1038 std::fs::write(&pack, &bytes).unwrap();
1039 let store = PackStore::open(&pack).unwrap();
1040 for i in 0..5 {
1041 assert_eq!(store.get(&format!("p/{i}")).unwrap(), Some(body(i)));
1042 }
1043 store.put("p/5", &body(5)).unwrap();
1044 assert!(std::fs::read(&pack).unwrap().starts_with(PACK_MAGIC_V1));
1045 store.compact().unwrap();
1046 assert!(std::fs::read(&pack).unwrap().starts_with(PACK_MAGIC));
1047 drop(store);
1048 let reader = PackStore::open_read(&pack).unwrap();
1049 for i in 0..6 {
1050 assert_eq!(reader.get(&format!("p/{i}")).unwrap(), Some(body(i)));
1051 }
1052 }
1053}