Skip to main content

rfc9162_tiles/
pack.rs

1//! One file for every store path: a packed [`TileStore`], so a log costs a
2//! constant number of inodes however long its history.
3//!
4//! The pack is an append-only file. Each record carries a store path and
5//! either its bytes, a deletion, or a rename, and ends in a SHA-256 over the
6//! record, so a torn tail is found and ignored. Bytes that shrink under
7//! DEFLATE are stored compressed, record by record; hash tiles do not
8//! shrink and are stored as they are. A path's current bytes are the last
9//! record that names it, so [`PackStore::compact`] rewrites the pack with
10//! one record per live path and renames it over the old one.
11//!
12//! A compaction also writes an index as the pack's first record: every live
13//! path with where its body sits, and the length of the pack the index
14//! covers. Opening such a pack reads the index and then only the records
15//! appended after the region it covers, rather than every record; a record
16//! the index named is checked against its digest when it is read. Packs
17//! written by format v1 (no index) open by reading every record, and become
18//! v2 at their next compaction.
19//!
20//! Writers that share a pack ([`PackStore::open_shared`]) take
21//! `<pack>.lock` for each change rather than for their lifetime: under the
22//! lock a writer reads what the others appended, drops a torn tail a crashed
23//! writer left, applies its change and releases. A shared writer and an
24//! exclusive one ([`PackStore::open`]) still exclude each other through the
25//! same lock file.
26//!
27//! One writer at a time: the writer holds `<pack>.lock`, created
28//! exclusively and removed when the store is dropped, the way the tile
29//! log's own locks work; a crash leaves it, and [`PackStore::break_lock`]
30//! removes it once the operator knows no writer is alive. Readers open the
31//! pack without a lock and see every record whose digest holds; a reader
32//! re-reads the tail before each operation, so it sees what a writer has
33//! appended since, and reopens the pack after a compaction.
34
35use 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
46/// The first bytes of a pack file as this version writes it (format v2,
47/// whose first record may be an index).
48pub const PACK_MAGIC: &[u8] = b"rfc9162-tiles/pack/v2\n";
49/// The first bytes of a pack file in format v1, which still opens.
50pub 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;
55/// The live table a compaction writes as the first record of a v2 pack.
56const KIND_INDEX: u8 = 4;
57
58const CODEC_RAW: u8 = 0;
59const CODEC_DEFLATE: u8 = 1;
60
61/// Bodies shorter than this are stored raw: the DEFLATE framing costs more
62/// than it saves.
63const COMPRESS_FROM: usize = 128;
64const DEFLATE_LEVEL: u8 = 6;
65
66/// kind, codec, path length, stored length, raw length.
67const HEAD_LEN: usize = 1 + 1 + 2 + 4 + 4;
68const DIGEST_LEN: usize = 32;
69
70/// An index record's body: the covered length and the path count.
71const INDEX_FIXED: usize = 8 + 4;
72/// Per path in the index: path length, record start, body offset, stored,
73/// raw, codec.
74const INDEX_PER_PATH: usize = 2 + 8 + 8 + 4 + 4 + 1;
75
76/// Bytes the index record of a compacted pack takes for `paths` live paths
77/// whose names total `names` bytes.
78fn index_len(paths: usize, names: usize) -> u64 {
79    (HEAD_LEN + INDEX_FIXED + DIGEST_LEN + paths * INDEX_PER_PATH + names) as u64
80}
81
82/// Where a path's bytes sit in the pack.
83#[derive(Debug, Clone, Copy)]
84struct Slot {
85    /// Offset of the record that holds the body; a rename keeps the record,
86    /// whose path is then the old one.
87    start: u64,
88    /// Offset of the stored body.
89    offset: u64,
90    stored: u32,
91    raw: u32,
92    codec: u8,
93}
94
95struct Inner {
96    file: File,
97    /// Length of the pack up to and including the last record whose digest
98    /// held. Records past it are a torn tail.
99    valid_len: u64,
100    live: BTreeMap<String, Slot>,
101    /// Bytes the live records occupy, body and framing, plus the magic and
102    /// the index a compaction writes: the pack's length after a compaction.
103    live_bytes: u64,
104    #[cfg(unix)]
105    inode: u64,
106}
107
108/// A [`TileStore`] over one append-only file.
109pub struct PackStore {
110    path: PathBuf,
111    writer: bool,
112    /// Takes the lock per change ([`PackStore::open_shared`]) and waits up to
113    /// this long for it; `None` for an exclusive writer or a reader.
114    shared: Option<Duration>,
115    inner: Mutex<Inner>,
116}
117
118/// How long a shared writer waits for `<pack>.lock` before giving up.
119pub const SHARED_LOCK_WAIT: Duration = Duration::from_secs(30);
120
121/// `<pack>.lock` held for one change; removed when dropped.
122struct ChangeLock(PathBuf);
123
124impl Drop for ChangeLock {
125    fn drop(&mut self) {
126        let _ = fs::remove_file(&self.0);
127    }
128}
129
130/// What a compaction did.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub struct Compacted {
133    /// Pack length before.
134    pub before: u64,
135    /// Pack length after.
136    pub after: u64,
137    /// Live paths carried over.
138    pub paths: usize,
139}
140
141impl PackStore {
142    /// Opens the pack at `path` for writing, creating it when absent, and
143    /// takes `<path>.lock`. A pack another writer holds is refused with
144    /// [`io::ErrorKind::WouldBlock`].
145    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        // A writer owns the tail: whatever a crash left past the last good
161        // record is dropped before anything is appended after it.
162        {
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    /// Opens the pack at `path` as one of several writers, creating it when
174    /// absent. No lock is held between changes; each change waits up to
175    /// [`SHARED_LOCK_WAIT`] for `<path>.lock` and fails with
176    /// [`io::ErrorKind::WouldBlock`] after that.
177    pub fn open_shared(path: impl Into<PathBuf>) -> io::Result<Self> {
178        Self::open_shared_waiting(path, SHARED_LOCK_WAIT)
179    }
180
181    /// [`PackStore::open_shared`] with its own wait for the lock.
182    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        // Creating the file writes its magic, which happens under the lock so
190        // two writers creating it at once do not both write it.
191        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    /// Opens the pack at `path` for reading, without a lock. The pack must
201    /// exist.
202    pub fn open_read(path: impl Into<PathBuf>) -> io::Result<Self> {
203        Self::open_with(path.into(), false)
204    }
205
206    /// Removes a lock a crashed writer left. The caller knows no writer is
207    /// alive; nothing here checks.
208    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    /// The pack file.
217    #[must_use]
218    pub fn path(&self) -> &Path {
219        &self.path
220    }
221
222    /// Length of the pack file, including any torn tail.
223    pub fn len(&self) -> io::Result<u64> {
224        Ok(fs::metadata(&self.path)?.len())
225    }
226
227    /// Whether the pack holds no path.
228    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    /// Bytes the live records occupy, framing, magic and index included: the
235    /// pack after a compaction is exactly this long.
236    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    /// Rewrites the pack with one record per live path, in path order, and
243    /// renames it over the old one. The tree and every proof are untouched:
244    /// they are over entry bytes, and every live path keeps its bytes.
245    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 &current {
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    /// For a shared writer, takes the lock, reads what other writers appended
335    /// and drops a torn tail a crashed writer left; the change follows while
336    /// the returned lock is held. Nothing for an exclusive writer.
337    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    /// Reads records a writer appended since, or reopens the pack when a
351    /// compaction replaced the file. An exclusive writer's view is its own.
352    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
404/// Opens the pack file and reads its records.
405fn 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
575/// Takes `lock` for one change, retrying with a growing pause until `wait`
576/// has passed.
577fn 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
615/// The framing of a record whose stored body is already encoded.
616fn 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
634/// The stored body of the `PUT` record at `slot`, checked against the
635/// record's digest and head: a record an index named was not read when the
636/// pack opened, so it is checked here.
637fn 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
679/// Bytes one live path costs a compacted pack: its record and its index row.
680fn 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
698/// The index record of a compacted pack: the length it covers and, per live
699/// path in path order, where the body sits.
700fn 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
722/// Takes the live table from a v2 pack's index record when its digest
723/// holds, every slot lies inside the covered region and the covered region
724/// lies inside the file; `valid_len` then moves to the end of that region.
725/// A missing or unusable index leaves `inner` as it was, and the scan reads
726/// every record.
727fn 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
767/// Steps over a first record that says it is an index but could not be
768/// used (a digest or a bound that did not hold), by the lengths its head
769/// declares, so the scan applies the records after it rather than stopping
770/// at it.
771fn 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
787/// The covered length and the slots of an index body, or `None` when the
788/// body is malformed or a slot does not lie between `first` and the covered
789/// length.
790fn 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
825/// Reads records from `valid_len` to the end of the file one at a time,
826/// applying each whose digest holds and stopping at the first that does
827/// not: a short read, a bad digest, a path that is not UTF-8, or a kind
828/// this version does not know.
829fn 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            // An index carries no change of its own; when it was not usable
880            // the records it describes follow it and are applied here.
881            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        // A byte inside the index body; its digest no longer holds.
993        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}