sidereon_core/artifact_bytes.rs
1//! Backing storage for readers over large binary artifacts.
2//!
3//! The terrain store and the precise-interpolant store are both read by
4//! indexing into a byte span: construction parses only the header, datum tag,
5//! and tile/segment index, and every lookup addresses payload by offset. Neither
6//! reader holds a reference into its own bytes, so the bytes can be owned,
7//! borrowed, or memory-mapped without any of them being a self-referential
8//! struct - the span is derived on demand from whichever backing is present.
9//!
10//! That is what makes the mapped variant safe to add: there is no interior
11//! pointer to keep valid, no drop-order invariant hiding in field declaration
12//! order, and no `unsafe` at any interface boundary.
13//!
14//! # Why mapping matters more than the copy
15//!
16//! Avoiding one `memcpy` is the smaller half. A memory map is demand-paged, so a
17//! reader that queries a geographically local region faults in the handful of
18//! pages covering those tiles and never touches the rest of the file. A
19//! constructor that copies - however the bytes arrive - forfeits that and pays
20//! for the whole artifact on every open. For a 30+ GB terrain store that is the
21//! difference between a working process and one that cannot start.
22
23use std::borrow::Cow;
24
25/// Where a reader's bytes live.
26///
27/// `Mapped` is only available with the `mmap` feature, so the default build
28/// carries no additional dependency and targets where mapping is meaningless
29/// (wasm) simply do not enable it.
30#[derive(Debug, Clone)]
31pub enum ArtifactBytes<'a> {
32 /// A span the caller owns and keeps alive.
33 Borrowed(&'a [u8]),
34 /// A vector this reader owns.
35 Owned(Vec<u8>),
36 /// A read-only memory map this reader owns.
37 ///
38 /// Shared behind an `Arc` so the reader stays cheap to clone; a map is a
39 /// kernel-level resource and duplicating it per clone would be wasteful.
40 #[cfg(feature = "mmap")]
41 Mapped(std::sync::Arc<memmap2::Mmap>),
42}
43
44impl<'a> ArtifactBytes<'a> {
45 /// Borrow the artifact bytes.
46 #[must_use]
47 pub fn as_slice(&self) -> &[u8] {
48 match self {
49 Self::Borrowed(bytes) => bytes,
50 Self::Owned(bytes) => bytes.as_slice(),
51 #[cfg(feature = "mmap")]
52 Self::Mapped(map) => &map[..],
53 }
54 }
55
56 /// Whether these bytes are a memory map rather than a copy in process
57 /// memory.
58 ///
59 /// Exposed so a caller - or a test - can assert that a path-based open
60 /// actually mapped the file instead of reading it. A change that quietly
61 /// relocated the copy would otherwise be indistinguishable from a fix.
62 #[must_use]
63 pub fn is_memory_mapped(&self) -> bool {
64 #[cfg(feature = "mmap")]
65 {
66 matches!(self, Self::Mapped(_))
67 }
68 #[cfg(not(feature = "mmap"))]
69 {
70 false
71 }
72 }
73}
74
75impl AsRef<[u8]> for ArtifactBytes<'_> {
76 fn as_ref(&self) -> &[u8] {
77 self.as_slice()
78 }
79}
80
81impl<'a> From<Cow<'a, [u8]>> for ArtifactBytes<'a> {
82 fn from(bytes: Cow<'a, [u8]>) -> Self {
83 match bytes {
84 Cow::Borrowed(bytes) => Self::Borrowed(bytes),
85 Cow::Owned(bytes) => Self::Owned(bytes),
86 }
87 }
88}
89
90/// Map a file read-only.
91///
92/// These artifacts are content-addressed and are mounted read-only where they
93/// are deployed, so the map is never opened for writing.
94///
95/// # Safety of the underlying map
96///
97/// `memmap2::Mmap::map` is unsafe because the mapped file can be modified by
98/// another process, which would change bytes under the reader. The contract
99/// here is the same one the format already relies on: an artifact is
100/// content-addressed and immutable once published. A caller that maps a file
101/// somebody else is concurrently rewriting has a corrupt read either way; the
102/// map does not introduce that hazard, it inherits it.
103#[cfg(feature = "mmap")]
104pub fn map_file_read_only(path: &std::path::Path) -> std::io::Result<ArtifactBytes<'static>> {
105 let file = std::fs::File::open(path)?;
106 // SAFETY: opened read-only, and the artifact is immutable once published -
107 // see the note above.
108 let map = unsafe { memmap2::Mmap::map(&file)? };
109 Ok(ArtifactBytes::Mapped(std::sync::Arc::new(map)))
110}