Skip to main content

aube_store/
cas.rs

1use crate::{Error, Store, StoredFile};
2use decmpfs::Gate;
3use std::cell::RefCell;
4use std::path::{Path, PathBuf};
5use std::sync::atomic::Ordering;
6
7thread_local! {
8    static B3_HASHER: RefCell<blake3::Hasher> = RefCell::new(blake3::Hasher::new());
9}
10
11/// Per-shard mutex array used by the macOS CAS fast path to serialize
12/// concurrent writers within a single process. Indexed by the first
13/// byte of the file's BLAKE3 hash (matching the on-disk 2-char shard
14/// layout), so two threads writing the same hash always collide; threads
15/// writing different hashes typically don't. The array is process-global
16/// rather than per-`Store` because there is at most one active store
17/// per install, and a static avoids carrying 256 mutexes in every cheap
18/// `Store::clone()` along the fetch pipeline.
19///
20/// macOS-gated rather than `not(linux)` because the fast-path block
21/// itself uses `OpenOptionsExt::mode`, which only exists on Unix —
22/// Windows would fail to compile under `not(linux)`. Linux already has
23/// `O_TMPFILE + linkat` (atomic-by-construction, faster than either
24/// alternative); Windows keeps the tempfile + persist_noclobber path.
25#[cfg(target_os = "macos")]
26static FAST_PATH_SHARD_LOCKS: [std::sync::Mutex<()>; 256] =
27    [const { std::sync::Mutex::new(()) }; 256];
28
29/// Recursively copy `src` into `dst`. Used only by the one-shot
30/// legacy-index migration fallback when `rename` fails (typically
31/// cross-filesystem). Not a hot path; correctness > speed.
32pub(crate) fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
33    std::fs::create_dir_all(dst)?;
34    for entry in std::fs::read_dir(src)? {
35        let entry = entry?;
36        let file_type = entry.file_type()?;
37        let from = entry.path();
38        let to = dst.join(entry.file_name());
39        if file_type.is_dir() {
40            copy_dir_recursive(&from, &to)?;
41        } else if file_type.is_file() {
42            std::fs::copy(&from, &to)?;
43        }
44        // Symlinks and other types are skipped — the index cache only
45        // ever contains regular JSON files (optionally under a single
46        // level of integrity-shard subdirs).
47    }
48    Ok(())
49}
50
51pub(crate) fn blake3_hex(content: &[u8]) -> String {
52    B3_HASHER.with(|cell| {
53        let mut h = cell.borrow_mut();
54        h.reset();
55        h.update(content);
56        h.finalize().to_hex().to_string()
57    })
58}
59
60pub(crate) fn cas_file_matches_len(path: &Path, expected_len: u64) -> bool {
61    path.metadata()
62        .map(|metadata| metadata.len() == expected_len)
63        .unwrap_or(false)
64}
65
66fn wait_for_cas_file_len(path: &Path, expected_len: u64) {
67    let deadline = std::time::Instant::now() + std::time::Duration::from_millis(50);
68    while !cas_file_matches_len(path, expected_len) && std::time::Instant::now() < deadline {
69        std::thread::sleep(std::time::Duration::from_micros(250));
70    }
71}
72
73/// The opt-in store-compression gate, resolved once from the
74/// environment. Returns `Some(gate)` only when `AUBE_COMPRESS_STORE` is
75/// set; otherwise `None` and the CAS write path is byte-for-byte the
76/// pre-existing one.
77///
78/// The toggle's value selects the gate:
79///   - `AUBE_COMPRESS_STORE=1` (or any non-`size:`/non-`glob:` value) →
80///     the fleet default `**/*.node`, no size floor.
81///   - `AUBE_COMPRESS_STORE="glob:<pat>"` → that glob, no size floor.
82///   - `AUBE_COMPRESS_STORE="size:<pred>"` → `**/*.node` AND a size
83///     predicate (e.g. `size:>= 1MB`).
84///   - `AUBE_COMPRESS_STORE="glob:<pat>;size:<pred>"` → both.
85///
86/// A malformed size predicate disables compression (returns `None`)
87/// rather than silently widening the gate; the addon still lands plain.
88pub(crate) fn store_compression_gate() -> Option<&'static Gate> {
89    static GATE: std::sync::OnceLock<Option<Gate>> = std::sync::OnceLock::new();
90    GATE.get_or_init(|| {
91        let raw = aube_util::env::embedder_env("COMPRESS_STORE")?;
92        parse_compress_store_gate(&raw.to_string_lossy())
93    })
94    .as_ref()
95}
96
97/// Pure parse of an `AUBE_COMPRESS_STORE` value into a `Gate`. Split out
98/// so the directive grammar is unit-testable without the process-global
99/// env `OnceLock`. `None` means "no gate" (compression off): the env var
100/// being *unset* short-circuits in the caller, but a set-but-empty value
101/// (`AUBE_COMPRESS_STORE=`) reaches here and is treated as affirmative
102/// (the default `**/*.node` gate). A malformed size predicate fails closed.
103pub(crate) fn parse_compress_store_gate(spec: &str) -> Option<Gate> {
104    let trimmed = spec.trim();
105    // A bare/affirmative value means "use the fleet default gate".
106    if trimmed.is_empty() || matches!(trimmed, "1" | "true" | "on" | "yes") {
107        return Some(Gate::default());
108    }
109    let mut glob: Option<&str> = None;
110    let mut size: Option<&str> = None;
111    for part in trimmed.split(';') {
112        let part = part.trim();
113        if let Some(rest) = part.strip_prefix("glob:") {
114            glob = Some(rest.trim());
115        } else if let Some(rest) = part.strip_prefix("size:") {
116            size = Some(rest.trim());
117        }
118    }
119    // No recognized directive → treat the value as affirmative.
120    if glob.is_none() && size.is_none() {
121        return Some(Gate::default());
122    }
123    match Gate::new(glob.or(Some(decmpfs::DEFAULT_GLOB)), size) {
124        Ok(gate) => Some(gate),
125        Err(err) => {
126            warn!(
127                "AUBE_COMPRESS_STORE has an invalid size predicate ({err}); \
128                 store compression disabled"
129            );
130            None
131        }
132    }
133}
134
135#[cfg(test)]
136mod compress_gate_tests {
137    use super::parse_compress_store_gate;
138
139    #[test]
140    fn affirmative_and_directives_yield_a_gate() {
141        // Bare/affirmative or unrecognized values → the fleet default gate.
142        for spec in ["", "1", "true", "on", "yes", "whatever"] {
143            assert!(
144                parse_compress_store_gate(spec).is_some(),
145                "spec {spec:?} should produce a gate"
146            );
147        }
148        // Explicit glob / size directives parse into a gate.
149        assert!(parse_compress_store_gate("glob:**/*.so").is_some());
150        assert!(parse_compress_store_gate("size:>= 1MB").is_some());
151        assert!(parse_compress_store_gate("glob:**/*.node;size:>= 512KB").is_some());
152    }
153
154    #[test]
155    fn malformed_size_predicate_fails_closed() {
156        // A bad size predicate disables compression (None) rather than
157        // silently widening the gate — the addon still lands plain.
158        assert!(parse_compress_store_gate("size:banana").is_none());
159    }
160}
161
162/// Outcome of `create_cas_file`. `Created` means we wrote the bytes
163/// at the final path; `AlreadyExisted` means another writer (or a
164/// previous import) had already committed bit-identical content. The
165/// distinction lets `import_bytes` skip the post-write length check
166/// on the freshly-created path — the file IS exactly the bytes we
167/// just wrote.
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169enum CasWriteOutcome {
170    Created,
171    AlreadyExisted,
172}
173
174impl Store {
175    /// Ensure every two-char shard directory under the CAS root exists.
176    /// CAS files live under `<root>/<ab>/<cdef...>` for 256 possible
177    /// prefixes. Running this once before a batch of `import_bytes`
178    /// calls lets the per-file hot path skip the `mkdirp(parent)` stat
179    /// entirely (the parent is guaranteed to exist). On APFS that
180    /// removes ~7.5k redundant `stat` syscalls per cold install — the
181    /// `mkdirp` inside `xx::file::write` was the #1 stat hotspot in a
182    /// dtrace profile.
183    ///
184    /// Cheap to call repeatedly: each `create_dir_all` is a no-op when
185    /// the directory already exists, but callers should still hoist the
186    /// call out of tight loops.
187    pub fn ensure_shards_exist(&self) -> Result<(), Error> {
188        self.prepare_for_write()?;
189        std::fs::create_dir_all(&self.root).map_err(|e| Error::Io(self.root.clone(), e))?;
190        // Windows Defender and Search both touch every file in the
191        // store on default installs. Setting this attribute makes
192        // them skip. Non-NTFS volumes ignore it harmlessly.
193        aube_util::fs::set_not_content_indexed(&self.root);
194        let mut buf = [0u8; 2];
195        for hi in 0u8..16 {
196            for lo in 0u8..16 {
197                buf[0] = hex_digit(hi);
198                buf[1] = hex_digit(lo);
199                // SAFETY: every byte in `buf` comes from `hex_digit`,
200                // which only emits `0-9` / `a-f` — always valid UTF-8.
201                let shard = std::str::from_utf8(&buf).unwrap();
202                let path = self.root.join(shard);
203                std::fs::create_dir_all(&path).map_err(|e| Error::Io(path, e))?;
204            }
205        }
206        Ok(())
207    }
208
209    /// Atomically create `path` without overwriting an existing CAS entry.
210    /// `AlreadyExists` is a no-op here; callers that know the expected content
211    /// length must verify it before trusting a reused path. Non-empty files are
212    /// written through a sibling temp file and persisted with no-clobber
213    /// semantics so an interrupted import cannot leave a torn file at the
214    /// content-addressed path. We intentionally do not fsync every CAS file:
215    /// cold installs import tens of thousands of files, and package-index
216    /// loading rejects missing/truncated entries so they can be fetched again.
217    /// `NotFound` means a concurrent prune or a missed `ensure_shards_exist`
218    /// removed the parent shard; recreate it and retry exactly once before
219    /// surfacing.
220    fn create_cas_file(
221        &self,
222        path: &Path,
223        content: Option<&[u8]>,
224    ) -> Result<CasWriteOutcome, Error> {
225        fn do_create_and_write(
226            this: &Store,
227            path: &Path,
228            content: Option<&[u8]>,
229        ) -> Result<CasWriteOutcome, Error> {
230            if let Some(bytes) = content {
231                // O_TMPFILE creates anon file in parent, linkat
232                // publishes atomically. Skips mkstemp uniqueness probe
233                // and post-write fchmod. Docker overlayfs hits the
234                // EOPNOTSUPP fallback. AUBE_DISABLE_O_TMPFILE for
235                // regression cover.
236                #[cfg(target_os = "linux")]
237                {
238                    static O_TMPFILE_DISABLED: std::sync::OnceLock<bool> =
239                        std::sync::OnceLock::new();
240                    let disabled = *O_TMPFILE_DISABLED.get_or_init(|| {
241                        aube_util::env::embedder_env("DISABLE_O_TMPFILE").is_some()
242                    });
243                    if !disabled {
244                        match try_o_tmpfile_publish(path, bytes) {
245                            Ok(outcome) => return Ok(outcome),
246                            Err(OTmpfileFallback::Unsupported) => {}
247                            Err(OTmpfileFallback::Hard(e)) => return Err(e),
248                        }
249                    }
250                }
251
252                // macOS fast path: direct O_CREAT|O_EXCL at the final
253                // content-addressed path, no tempfile dance. Caller (the
254                // install command) flips `fast_path` on only after
255                // acquiring an exclusive store-level lock against other
256                // aube processes. We additionally serialize writers
257                // *within* this process per shard: two threads importing
258                // the same hash (a CAS-dedupe across packages, 35% of
259                // files on dep-heavy graphs like MUI/CodeMirror) would
260                // otherwise both attempt create_new — the loser sees an
261                // EEXIST against the winner's still-empty fd and the
262                // caller's size-mismatch recovery in `import_bytes` would
263                // unlink the file out from under the still-writing
264                // winner. The shard mutex sequences the open+write so the
265                // loser only observes the file at its final size.
266                //
267                // Crashed-predecessor recovery (the unlink+rewrite path
268                // that the slow path defers to `import_bytes`) runs here
269                // while the mutex is still held, so the caller's recovery
270                // can safely no-op for fast-path writes.
271                //
272                // On APFS the fast path is ~2.25x faster than
273                // tempfile+chmod+persist (~64µs/file vs ~145µs/file in
274                // isolation). macOS-gated rather than `not(linux)`
275                // because `OpenOptionsExt::mode` is unix-only — Windows
276                // keeps the tempfile path.
277                #[cfg(target_os = "macos")]
278                if this.fast_path.load(Ordering::Acquire) {
279                    use std::io::Write;
280                    use std::os::unix::fs::OpenOptionsExt;
281
282                    let shard_idx = path
283                        .parent()
284                        .and_then(|p| p.file_name())
285                        .and_then(|s| s.to_str())
286                        .and_then(|s| u8::from_str_radix(s, 16).ok())
287                        .map(|b| b as usize);
288                    // Every path produced by `file_path_from_hex` lives
289                    // under a 2-char hex shard, so this is the contract
290                    // every fast-path caller satisfies today. The assert
291                    // pins the invariant; if a future caller hands in a
292                    // non-CAS path, release builds skip the fast path
293                    // (falling through to the safe tempfile branch)
294                    // rather than do an unsynchronized write that could
295                    // race with another thread on the same hash.
296                    debug_assert!(
297                        shard_idx.is_some(),
298                        "fast-path CAS write to path without a valid hex shard parent: {}",
299                        path.display()
300                    );
301                    if let Some(i) = shard_idx {
302                        // Mutex poisoning is impossible here — the guard
303                        // is dropped at end of scope without us panicking
304                        // inside, so we either return cleanly or propagate
305                        // an `Err` while still releasing the lock. If a
306                        // future caller panics inside, `unwrap_or_else`
307                        // recovers the guard anyway.
308                        let _shard_guard = FAST_PATH_SHARD_LOCKS[i]
309                            .lock()
310                            .unwrap_or_else(|p| p.into_inner());
311
312                        // `OpenOptionsExt::mode(0o644)` is masked by the
313                        // process umask, so a non-default umask (e.g.
314                        // 0o077) would give CAS files 0o600. The
315                        // tempfile path uses `fchmod`, which ignores
316                        // umask. Match it with an explicit
317                        // `set_permissions` so the same store can't end
318                        // up with mixed-mode files depending on which
319                        // path wrote each entry.
320                        use std::os::unix::fs::PermissionsExt;
321                        let force_mode = std::fs::Permissions::from_mode(0o644);
322                        let open_result = std::fs::OpenOptions::new()
323                            .mode(0o644)
324                            .create_new(true)
325                            .write(true)
326                            .open(path);
327                        match open_result {
328                            Ok(mut f) => {
329                                f.set_permissions(force_mode.clone())
330                                    .map_err(|e| Error::Io(path.to_path_buf(), e))?;
331                                f.write_all(bytes)
332                                    .map_err(|e| Error::Io(path.to_path_buf(), e))?;
333                                return Ok(CasWriteOutcome::Created);
334                            }
335                            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
336                                // Holding the shard lock, so any in-process
337                                // writer for this hash already finished. If
338                                // the file size matches, it's a genuine
339                                // dedupe. If not, it's a crashed-predecessor
340                                // remnant — unlink and rewrite inline.
341                                if cas_file_matches_len(path, bytes.len() as u64) {
342                                    return Ok(CasWriteOutcome::AlreadyExisted);
343                                }
344                                let _ = xx::file::remove_file(path);
345                                match std::fs::OpenOptions::new()
346                                    .mode(0o644)
347                                    .create_new(true)
348                                    .write(true)
349                                    .open(path)
350                                {
351                                    Ok(mut f) => {
352                                        f.set_permissions(force_mode)
353                                            .map_err(|e| Error::Io(path.to_path_buf(), e))?;
354                                        f.write_all(bytes)
355                                            .map_err(|e| Error::Io(path.to_path_buf(), e))?;
356                                        return Ok(CasWriteOutcome::Created);
357                                    }
358                                    Err(e) => {
359                                        return Err(Error::Io(path.to_path_buf(), e));
360                                    }
361                                }
362                            }
363                            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
364                                // Shard dir missing — fall through to the slow
365                                // path; the outer wrapper will create_dir_all
366                                // and retry once.
367                            }
368                            Err(e) => return Err(Error::Io(path.to_path_buf(), e)),
369                        }
370                    }
371                }
372
373                // Tempfile + persist_noclobber gives atomic crash
374                // semantics: a partial write on `tmp` is dropped by
375                // tempfile's Drop impl, so the final path either
376                // contains the complete bytes or doesn't exist. A
377                // direct O_CREAT|O_EXCL write to the final path was
378                // tried (faster path, ~3 syscalls per file) but
379                // raced with concurrent installs in CI where two
380                // processes saw the same partial file in different
381                // orders and clobbered each other's recovery. The
382                // fast-path branch above re-enables it under an
383                // exclusive store lock.
384                let _ = this; // suppress unused warning on Linux
385                let parent = path.parent().ok_or_else(|| {
386                    Error::Io(path.to_path_buf(), std::io::ErrorKind::NotFound.into())
387                })?;
388                let mut tmp = tempfile::Builder::new()
389                    .prefix(".aube-cas-")
390                    .tempfile_in(parent)
391                    .map_err(|e| Error::Io(path.to_path_buf(), e))?;
392                use std::io::Write;
393                tmp.write_all(bytes)
394                    .map_err(|e| Error::Io(path.to_path_buf(), e))?;
395                #[cfg(unix)]
396                {
397                    use std::os::unix::fs::PermissionsExt;
398                    tmp.as_file()
399                        .set_permissions(std::fs::Permissions::from_mode(0o644))
400                        .map_err(|e| Error::Io(path.to_path_buf(), e))?;
401                }
402                return match tmp.persist_noclobber(path) {
403                    Ok(_) => Ok(CasWriteOutcome::Created),
404                    Err(e) if e.error.kind() == std::io::ErrorKind::AlreadyExists => {
405                        Ok(CasWriteOutcome::AlreadyExisted)
406                    }
407                    Err(e) => Err(Error::Io(path.to_path_buf(), e.error)),
408                };
409            }
410
411            match std::fs::OpenOptions::new()
412                .write(true)
413                .create_new(true)
414                .open(path)
415            {
416                Ok(_) => Ok(CasWriteOutcome::Created),
417                Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
418                    Ok(CasWriteOutcome::AlreadyExisted)
419                }
420                Err(e) => Err(Error::Io(path.to_path_buf(), e)),
421            }
422        }
423
424        match do_create_and_write(self, path, content) {
425            Ok(outcome) => Ok(outcome),
426            Err(Error::Io(_, ref ioe)) if ioe.kind() == std::io::ErrorKind::NotFound => {
427                // Shard dir missing. `ensure_shards_exist` normally
428                // pre-creates all 256 shards; this only fires when the
429                // caller didn't call it or a concurrent prune wiped
430                // the tree mid-install.
431                if let Some(parent) = path.parent() {
432                    std::fs::create_dir_all(parent)
433                        .map_err(|e| Error::Io(parent.to_path_buf(), e))?;
434                }
435                do_create_and_write(self, path, content)
436            }
437            Err(e) => Err(e),
438        }
439    }
440
441    /// Publish a tempfile that was written and BLAKE3-hashed while its tar
442    /// entry was decoded. The tempfile lives under the CAS root, so
443    /// `persist_noclobber` is an atomic same-filesystem publish and does not
444    /// copy the entry a second time.
445    pub(crate) fn import_hashed_tempfile(
446        &self,
447        mut temp: tempfile::NamedTempFile,
448        hex_hash: String,
449        len: u64,
450        executable: bool,
451    ) -> Result<StoredFile, Error> {
452        let store_path = self.file_path_from_hex(&hex_hash);
453        let parent = store_path
454            .parent()
455            .ok_or_else(|| Error::Io(store_path.clone(), std::io::ErrorKind::NotFound.into()))?;
456        std::fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_path_buf(), e))?;
457
458        #[cfg(unix)]
459        {
460            use std::os::unix::fs::PermissionsExt;
461            temp.as_file()
462                .set_permissions(std::fs::Permissions::from_mode(0o644))
463                .map_err(|e| Error::Io(store_path.clone(), e))?;
464        }
465
466        // The macOS byte importer publishes directly into the final path while
467        // holding this same shard lock. Hold it through publication and any
468        // recovery so a streamed writer cannot unlink an in-progress file.
469        #[cfg(target_os = "macos")]
470        let _shard_guard = hex_hash
471            .get(..2)
472            .and_then(|shard| u8::from_str_radix(shard, 16).ok())
473            .map(|shard| {
474                FAST_PATH_SHARD_LOCKS[shard as usize]
475                    .lock()
476                    .unwrap_or_else(|poisoned| poisoned.into_inner())
477            });
478        #[cfg(target_os = "macos")]
479        let mut recovery_lock = None;
480
481        let mut retried_missing_parent = false;
482        let mut retried_torn_entry = false;
483        let outcome = loop {
484            match temp.persist_noclobber(&store_path) {
485                Ok(_) => break CasWriteOutcome::Created,
486                Err(e) if e.error.kind() == std::io::ErrorKind::NotFound => {
487                    temp = e.file;
488                    if retried_missing_parent {
489                        return Err(Error::Io(store_path.clone(), e.error));
490                    }
491                    // A concurrent prune may remove the shard after the
492                    // initial create. Match the buffered importer by
493                    // recreating it and retrying publication once.
494                    std::fs::create_dir_all(parent)
495                        .map_err(|e| Error::Io(parent.to_path_buf(), e))?;
496                    retried_missing_parent = true;
497                }
498                Err(e) if e.error.kind() == std::io::ErrorKind::AlreadyExists => {
499                    temp = e.file;
500                    if !cas_file_matches_len(&store_path, len) {
501                        wait_for_cas_file_len(&store_path, len);
502                    }
503                    // A slow-path process can observe a partial final file
504                    // owned by another process that holds the macOS install
505                    // lock and is writing directly. Wait for that process
506                    // before deciding the entry is torn. The in-process shard
507                    // mutex above separately serializes recovery threads in
508                    // this process.
509                    #[cfg(target_os = "macos")]
510                    if !self.fast_path.load(Ordering::Acquire)
511                        && !cas_file_matches_len(&store_path, len)
512                        && recovery_lock.is_none()
513                    {
514                        let lock_dir = self
515                            .root
516                            .parent()
517                            .map(Path::to_path_buf)
518                            .unwrap_or_else(|| self.root.clone());
519                        std::fs::create_dir_all(&lock_dir)
520                            .map_err(|e| Error::Io(lock_dir.clone(), e))?;
521                        let lock_path = lock_dir.join(".install.lock");
522                        let file = std::fs::OpenOptions::new()
523                            .create(true)
524                            .truncate(false)
525                            .write(true)
526                            .open(&lock_path)
527                            .map_err(|e| Error::Io(lock_path.clone(), e))?;
528                        file.lock().map_err(|e| Error::Io(lock_path.clone(), e))?;
529                        recovery_lock = Some(file);
530                    }
531                    if cas_file_matches_len(&store_path, len) {
532                        break CasWriteOutcome::AlreadyExisted;
533                    } else if retried_torn_entry {
534                        return Err(Error::Io(store_path.clone(), e.error));
535                    } else {
536                        // Match `import_bytes` recovery for a crashed predecessor:
537                        // retain our complete staging file, remove the torn CAS
538                        // entry, and retry the no-clobber publish once.
539                        let _ = xx::file::remove_file(&store_path);
540                        retried_torn_entry = true;
541                    }
542                }
543                Err(e) => return Err(Error::Io(store_path.clone(), e.error)),
544            }
545        };
546
547        if aube_util::diag::enabled() {
548            let name = match outcome {
549                CasWriteOutcome::Created => "cas_miss",
550                CasWriteOutcome::AlreadyExisted => "cas_hit",
551            };
552            aube_util::diag::instant_lazy(aube_util::diag::Category::Store, name, || {
553                format!(r#"{{"size":{len}}}"#)
554            });
555        }
556
557        if executable {
558            self.write_exec_marker(&store_path)?;
559        }
560        Ok(StoredFile {
561            hex_hash,
562            store_path,
563            executable,
564            size: Some(len),
565        })
566    }
567
568    /// Import a single file's content into the store. Returns the stored file info.
569    ///
570    /// Hot path on cold installs: callers should invoke
571    /// [`Store::ensure_shards_exist`] once before a batch of imports so
572    /// this function can skip the per-file `mkdirp`. When shards don't
573    /// exist yet, the `create_new` open will fail with `NotFound`; we
574    /// fall back to the slow path for correctness.
575    pub fn import_bytes(&self, content: &[u8], executable: bool) -> Result<StoredFile, Error> {
576        self.prepare_for_write()?;
577        let hash_t0 = std::time::Instant::now();
578        let hex_hash = blake3_hex(content);
579        if aube_util::diag::enabled() {
580            aube_util::diag::event_lazy(
581                aube_util::diag::Category::Store,
582                "blake3_hash",
583                hash_t0.elapsed(),
584                || format!(r#"{{"size":{}}}"#, content.len()),
585            );
586        }
587
588        let store_path = self.file_path_from_hex(&hex_hash);
589        let _diag_write =
590            aube_util::diag::Span::new(aube_util::diag::Category::Store, "import_bytes_write")
591                .with_meta_fn(|| format!(r#"{{"size":{}}}"#, content.len()));
592
593        // Fast path: open-with-create-new combines the existence check
594        // and the open into a single syscall. On a cold CAS this does
595        // one open(O_CREAT|O_EXCL|O_WRONLY) per file and replaces the
596        // previous stat+create pair (~15k redundant stats per cold
597        // install). On a warm CAS, concurrent writers are safe: EEXIST
598        // means another writer already materialized this content (same
599        // hash = same bytes), so we skip and share the entry.
600        //
601        // `Created` means we just wrote the bytes — they are exactly
602        // `content.len()` by construction, no need to re-stat. Only
603        // the `AlreadyExisted` branch can produce a torn file (from a
604        // crashed predecessor) so the length check runs there only.
605        let outcome = self.create_cas_file(&store_path, Some(content))?;
606        // Surface CAS dedup hit/miss to diag so cold vs warm vs partial
607        // installs can be classified post-hoc. `cas_hit` fires when an
608        // identical-content file already lived in the store; `cas_miss`
609        // fires when we just wrote new bytes.
610        if aube_util::diag::enabled() {
611            let name = match outcome {
612                CasWriteOutcome::Created => "cas_miss",
613                CasWriteOutcome::AlreadyExisted => "cas_hit",
614            };
615            aube_util::diag::instant_lazy(aube_util::diag::Category::Store, name, || {
616                format!(r#"{{"size":{}}}"#, content.len())
617            });
618        }
619        // The macOS fast path verifies the file size inline under its
620        // shard mutex before returning `AlreadyExisted`, so this
621        // recovery only needs to run when we took the tempfile path.
622        // Skipping it there also prevents a race where the recovery
623        // unlinks a file that another in-process thread is concurrently
624        // re-creating after observing the same crashed-predecessor.
625        //
626        // `cfg!(target_os = "macos")` matches the cfg gate on the only
627        // code path that flips `fast_path` to true (and on the inline
628        // recovery inside `create_cas_file`). Without the cfg!, a future
629        // caller setting the flag on Linux would silently disable this
630        // recovery — the Linux O_TMPFILE branch has no inline
631        // length-check substitute, so torn CAS files would be accepted.
632        let fast_path_handled_recovery =
633            cfg!(target_os = "macos") && self.fast_path.load(Ordering::Acquire);
634        if outcome == CasWriteOutcome::AlreadyExisted && !fast_path_handled_recovery {
635            // A length mismatch from this branch can mean either
636            //   (a) a crashed predecessor left a torn file (the recovery
637            //       case this code was originally written for), or
638            //   (b) on macOS, another *process* is currently writing to
639            //       the same path via the fast path (no atomic publish
640            //       at the final path, so its in-progress fd is visible
641            //       by name to other writers).
642            // Burning the file in case (b) would unlink the active
643            // writer's inode and trigger a cascading recovery race. Wait
644            // briefly (50ms is dozens of typical small-file writes) for
645            // the partial file to settle. If it stays mismatched past
646            // the deadline, treat it as (a) and recover.
647            if !cas_file_matches_len(&store_path, content.len() as u64) {
648                wait_for_cas_file_len(&store_path, content.len() as u64);
649            }
650            if !cas_file_matches_len(&store_path, content.len() as u64) {
651                let _ = xx::file::remove_file(&store_path);
652                self.create_cas_file(&store_path, Some(content))?;
653                if !cas_file_matches_len(&store_path, content.len() as u64) {
654                    let actual_len = store_path.metadata().map(|metadata| metadata.len()).ok();
655                    return Err(Error::Io(
656                        store_path.clone(),
657                        std::io::Error::other(format!(
658                            "CAS entry has wrong size after import: expected {} bytes, got {}",
659                            content.len(),
660                            actual_len
661                                .map(|len| format!("{len} bytes"))
662                                .unwrap_or_else(|| "missing file".to_owned())
663                        )),
664                    ));
665                }
666            }
667        }
668
669        if executable {
670            // Behavior note: this branch now runs unconditionally when
671            // `executable=true`, including when the content file
672            // already existed (`AlreadyExists` above). Previously the
673            // marker was only written in the fresh-content branch.
674            // The new shape is strictly more correct — if the same
675            // bytes are imported twice, once with `executable=false`
676            // and once with `true`, the marker should exist after the
677            // second call. Auditing the callers of the `-exec` marker:
678            //   - `aube-store::import_bytes` (this function, the only
679            //     writer).
680            //   - `aube-store` tests (assert the marker exists after
681            //     an `executable=true` import).
682            //   - `aube::commands::store` (`aube store prune`)
683            //     uses the marker to skip bumping the "freed bytes"
684            //     counter when unlinking exec-marker sidecars.
685            // No code path reads the marker to decide executability —
686            // that's carried in `StoredFile.executable`, threaded
687            // through the `PackageIndex` and the linker. So flipping
688            // a marker-absent-to-present for a shared hash is safe.
689            self.write_exec_marker(&store_path)?;
690        }
691
692        Ok(StoredFile {
693            hex_hash,
694            store_path,
695            executable,
696            size: Some(content.len() as u64),
697        })
698    }
699
700    /// Import a tar entry's content, applying OS-level transparent
701    /// compression to the entries the store-compression gate selects.
702    ///
703    /// When `AUBE_COMPRESS_STORE` is unset this is exactly
704    /// [`Store::import_bytes`] — same CAS key, same write path. When it
705    /// is set and `rel_path` + size match the gate, the entry is first
706    /// unwrapped if it is a napi `--compress` hybrid (so the CAS stores
707    /// the raw `.node`, not the wrapper) and then written into the CAS
708    /// as a transparently-compressed file in ONE pass via
709    /// [`decmpfs::compress_bytes`] — never a write-then-read-back. The
710    /// kernel decompresses on read, so the stored file keeps its logical
711    /// size and exact bytes; `cas_file_matches_len` and the BLAKE3 CAS
712    /// key are computed against that logical content, unchanged.
713    ///
714    /// Fail-soft: `compress_bytes` itself falls back to a plain atomic
715    /// write on an unsupported FS or any backend error, so a matched
716    /// entry always lands. The gate firing only changes how the bytes
717    /// are stored, never whether they are.
718    pub fn import_bytes_gated(
719        &self,
720        rel_path: &str,
721        content: &[u8],
722        executable: bool,
723    ) -> Result<StoredFile, Error> {
724        self.import_bytes_with_gate(rel_path, content, executable, store_compression_gate())
725    }
726
727    /// Gate-injectable core of [`Store::import_bytes_gated`]. `gate` of
728    /// `None` is the byte-identical pre-existing CAS path. Separated from
729    /// the public method so tests can drive the compressed path with an
730    /// explicit gate rather than racing the process-global env toggle.
731    pub(crate) fn import_bytes_with_gate(
732        &self,
733        rel_path: &str,
734        content: &[u8],
735        executable: bool,
736        gate: Option<&Gate>,
737    ) -> Result<StoredFile, Error> {
738        self.prepare_for_write()?;
739        let Some(gate) = gate else {
740            return self.import_bytes(content, executable);
741        };
742
743        // Unwrap a napi `--compress` hybrid to the raw addon before the
744        // gate's size check and the CAS hash — a non-hybrid `.node`
745        // returns `None`, so this is a no-op for ordinary addons and the
746        // bytes (and CAS key) are identical to the ungated path.
747        let unwrapped = decmpfs::addon::unwrap_if_hybrid(content);
748        let stored_bytes: &[u8] = unwrapped.as_deref().unwrap_or(content);
749
750        if !gate.matches(rel_path, stored_bytes.len() as u64) {
751            // Not a gated entry. Store the (possibly unwrapped) bytes
752            // through the normal CAS path so a hybrid still lands as its
753            // raw addon even when too small to compress.
754            return self.import_bytes(stored_bytes, executable);
755        }
756
757        let hash_t0 = std::time::Instant::now();
758        let hex_hash = blake3_hex(stored_bytes);
759        if aube_util::diag::enabled() {
760            aube_util::diag::event_lazy(
761                aube_util::diag::Category::Store,
762                "blake3_hash",
763                hash_t0.elapsed(),
764                || format!(r#"{{"size":{}}}"#, stored_bytes.len()),
765            );
766        }
767        let store_path = self.file_path_from_hex(&hex_hash);
768
769        // If a prior import already committed this content, reuse it —
770        // the file is already stored (compressed or not) and the kernel
771        // reads it back identically. Matches `import_bytes`'s CAS-dedupe.
772        if cas_file_matches_len(&store_path, stored_bytes.len() as u64) {
773            if aube_util::diag::enabled() {
774                aube_util::diag::instant_lazy(aube_util::diag::Category::Store, "cas_hit", || {
775                    format!(r#"{{"size":{}}}"#, stored_bytes.len())
776                });
777            }
778            return self.finish_gated(hex_hash, store_path, executable, stored_bytes.len());
779        }
780
781        // Ensure the shard exists; `compress_bytes` writes the final
782        // path directly (its own sibling-temp + rename), so it relies on
783        // the parent dir being present just like the slow CAS path.
784        if let Some(parent) = store_path.parent()
785            && !parent.exists()
786        {
787            std::fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_path_buf(), e))?;
788        }
789
790        // One-pass compressed write. The gate is honored inside
791        // `compress_bytes` too, but we pass `Gate::any()` because we have
792        // already matched against `rel_path` (a CAS path no longer
793        // carries the package-relative name the glob expects).
794        match decmpfs::compress_bytes(&store_path, stored_bytes, &Gate::any()) {
795            Ok(_) => {}
796            Err(err) => {
797                // A genuine I/O failure from the one-pass writer. Fall
798                // back to the fully-guarded CAS path so the install is
799                // never left without the file.
800                warn!(
801                    "decmpfs one-pass write failed for {} ({err}); \
802                     falling back to the plain CAS path",
803                    store_path.display()
804                );
805                return self.import_bytes(stored_bytes, executable);
806            }
807        }
808
809        // Guard against a torn/short write the same way `import_bytes`
810        // does. decmpfs verifies the kernel read-back equals the bytes
811        // for a compressed Outcome, but a fail-soft plain fallback inside
812        // `compress_bytes` could still race a concurrent writer.
813        if !cas_file_matches_len(&store_path, stored_bytes.len() as u64) {
814            wait_for_cas_file_len(&store_path, stored_bytes.len() as u64);
815        }
816        if !cas_file_matches_len(&store_path, stored_bytes.len() as u64) {
817            let _ = xx::file::remove_file(&store_path);
818            return self.import_bytes(stored_bytes, executable);
819        }
820
821        // New content just landed (compressed, or fail-soft plain) — mirror
822        // `import_bytes`'s `cas_miss` so gated installs classify identically.
823        if aube_util::diag::enabled() {
824            aube_util::diag::instant_lazy(aube_util::diag::Category::Store, "cas_miss", || {
825                format!(r#"{{"size":{}}}"#, stored_bytes.len())
826            });
827        }
828        self.finish_gated(hex_hash, store_path, executable, stored_bytes.len())
829    }
830
831    /// Write the sidecar `<store_path>-exec` marker that records a CAS entry
832    /// as executable. Shared by `import_bytes` and `finish_gated`.
833    fn write_exec_marker(&self, store_path: &Path) -> Result<(), Error> {
834        let exec_marker = PathBuf::from(format!("{}-exec", store_path.display()));
835        self.create_cas_file(&exec_marker, None)?;
836        Ok(())
837    }
838
839    /// Shared tail of `import_bytes_gated`: write the executable marker
840    /// (if any) and build the `StoredFile`. Mirrors the marker handling
841    /// in `import_bytes`.
842    fn finish_gated(
843        &self,
844        hex_hash: String,
845        store_path: PathBuf,
846        executable: bool,
847        len: usize,
848    ) -> Result<StoredFile, Error> {
849        if executable {
850            self.write_exec_marker(&store_path)?;
851        }
852        Ok(StoredFile {
853            hex_hash,
854            store_path,
855            executable,
856            size: Some(len as u64),
857        })
858    }
859}
860
861// Thin wrapper over posix_fallocate(3) which returns the error code
862// directly (does not set errno). Caller decides how to handle the
863// error. Existing call site uses `let _ = ...` to ignore EOPNOTSUPP /
864// ENOSYS / EINVAL on filesystems where pre-allocation is a no-op.
865//
866// `len` is `libc::off_t` because that's the type the underlying glibc
867// signature uses, and it varies per target: `i64` on 64-bit Linux and
868// on 32-bit Linux when the libc bindings opt into _FILE_OFFSET_BITS=64,
869// `i32` on 32-bit Linux otherwise (e.g. Debian/Ubuntu's armhf packaging
870// build env). Taking it directly keeps the call-site cast in one place.
871#[cfg(target_os = "linux")]
872fn posix_fallocate(file: &std::fs::File, len: libc::off_t) -> std::io::Result<()> {
873    use std::os::fd::AsRawFd;
874    if len <= 0 {
875        return Ok(());
876    }
877    // SAFETY: fd is owned by `file` for the duration of the call.
878    let r = unsafe { libc::posix_fallocate(file.as_raw_fd(), 0, len) };
879    if r == 0 {
880        Ok(())
881    } else {
882        Err(std::io::Error::from_raw_os_error(r))
883    }
884}
885
886// Unsupported means kernel/fs lacks O_TMPFILE, caller falls back.
887// Hard is a real I/O error that bubbles up.
888#[cfg(target_os = "linux")]
889enum OTmpfileFallback {
890    Unsupported,
891    Hard(Error),
892}
893
894// Size threshold below which we skip both `posix_fallocate` and
895// `posix_fadvise(DONTNEED)` on the CAS write path. Both are
896// fixed-cost-per-call best-effort advisory syscalls whose benefits
897// (avoid ext4 fragmentation, evict pages) don't apply to small
898// writes — the kernel won't fragment a single-block write, and tiny
899// pages don't meaningfully pressure the cache. samply on a cold
900// 1230-pkg install pinned the two at ~4.4% + ~4.8% of self time
901// before this gate; gating to ≥64KB skips them for >95% of npm
902// tarball entries while preserving the original behavior on the
903// large files (typescript.js, monaco-editor, etc.) where it pays.
904//
905// Overridable via `AUBE_CAS_SMALL_FILE_THRESHOLD` (bytes). Set to 0
906// to restore the always-on behavior; set to a very large number to
907// effectively disable both syscalls.
908#[cfg(target_os = "linux")]
909const CAS_SMALL_FILE_THRESHOLD_DEFAULT: usize = 64 * 1024;
910
911#[cfg(target_os = "linux")]
912fn cas_small_file_threshold() -> usize {
913    static THRESHOLD: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
914    *THRESHOLD.get_or_init(|| {
915        match aube_util::env::embedder_env("CAS_SMALL_FILE_THRESHOLD")
916            .as_deref()
917            .map(|s| s.to_string_lossy().into_owned())
918        {
919            None => CAS_SMALL_FILE_THRESHOLD_DEFAULT,
920            Some(raw) => raw.parse::<usize>().unwrap_or_else(|_| {
921                warn!(
922                    "CAS_SMALL_FILE_THRESHOLD={raw:?} is not a non-negative integer; \
923                     falling back to default {CAS_SMALL_FILE_THRESHOLD_DEFAULT}"
924                );
925                CAS_SMALL_FILE_THRESHOLD_DEFAULT
926            }),
927        }
928    })
929}
930
931// Open anonymous file in parent dir, write, linkat via /proc/self/fd.
932// Skips the tempfile unique-name probe and explicit fchmod. Falls
933// back via Unsupported on EOPNOTSUPP, ENOENT (no /proc), or EXDEV.
934// AUBE_DISABLE_O_TMPFILE forces the legacy path.
935#[cfg(target_os = "linux")]
936fn try_o_tmpfile_publish(path: &Path, bytes: &[u8]) -> Result<CasWriteOutcome, OTmpfileFallback> {
937    use std::ffi::CString;
938    use std::io::Write;
939    use std::os::fd::FromRawFd;
940    use std::os::unix::ffi::OsStrExt;
941    use std::os::unix::fs::PermissionsExt;
942
943    let parent = path.parent().ok_or(OTmpfileFallback::Hard(Error::Io(
944        path.to_path_buf(),
945        std::io::ErrorKind::NotFound.into(),
946    )))?;
947    let parent_c = CString::new(parent.as_os_str().as_bytes()).map_err(|_| {
948        OTmpfileFallback::Hard(Error::Io(
949            path.to_path_buf(),
950            std::io::Error::new(std::io::ErrorKind::InvalidInput, "parent path has nul"),
951        ))
952    })?;
953    // SAFETY: `parent_c` is valid for the duration of the call.
954    let raw_fd = unsafe {
955        libc::open(
956            parent_c.as_ptr(),
957            libc::O_TMPFILE | libc::O_RDWR | libc::O_CLOEXEC,
958            0o644 as libc::c_uint,
959        )
960    };
961    if raw_fd < 0 {
962        let err = std::io::Error::last_os_error();
963        return match err.raw_os_error() {
964            // Old kernels lack O_TMPFILE. Overlayfs/tmpfs return
965            // EOPNOTSUPP, EISDIR, or EINVAL on some kernels.
966            // ENOTSUP is the same value as EOPNOTSUPP on Linux.
967            Some(libc::EOPNOTSUPP) | Some(libc::EISDIR) | Some(libc::EINVAL) => {
968                Err(OTmpfileFallback::Unsupported)
969            }
970            _ => Err(OTmpfileFallback::Hard(Error::Io(path.to_path_buf(), err))),
971        };
972    }
973    // SAFETY: raw_fd is owned, OwnedFd closes on drop.
974    let owned = unsafe { std::os::fd::OwnedFd::from_raw_fd(raw_fd) };
975    let mut file = std::fs::File::from(owned);
976    let small_threshold = cas_small_file_threshold();
977    let is_large = bytes.len() >= small_threshold;
978    // Best-effort fallocate so the kernel allocates contiguous extents
979    // up front. Skips ext4 fragmentation churn on the next write.
980    // EOPNOTSUPP and ENOSYS are fine, regular write_all handles them.
981    // Skipped below `small_threshold`: fragmentation only matters for
982    // multi-block writes, and most npm tarball entries are well under
983    // that. See `cas_small_file_threshold` for rationale.
984    if is_large {
985        let _ = posix_fallocate(&file, bytes.len() as libc::off_t);
986    }
987    file.write_all(bytes)
988        .map_err(|e| OTmpfileFallback::Hard(Error::Io(path.to_path_buf(), e)))?;
989    file.set_permissions(std::fs::Permissions::from_mode(0o644))
990        .map_err(|e| OTmpfileFallback::Hard(Error::Io(path.to_path_buf(), e)))?;
991    // No sync_data: contradicts the no-fsync CAS policy. Crash window
992    // between write and linkat is acceptable, lockfile + state hash
993    // recovers the missing entry on next install.
994
995    let proc_link = format!("/proc/self/fd/{}", std::os::fd::AsRawFd::as_raw_fd(&file));
996    let proc_c = CString::new(proc_link.as_bytes()).map_err(|_| {
997        OTmpfileFallback::Hard(Error::Io(
998            path.to_path_buf(),
999            std::io::Error::other("fd path has nul"),
1000        ))
1001    })?;
1002    let final_c = CString::new(path.as_os_str().as_bytes()).map_err(|_| {
1003        OTmpfileFallback::Hard(Error::Io(
1004            path.to_path_buf(),
1005            std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has nul"),
1006        ))
1007    })?;
1008    // SAFETY: both CStrings live through the call. AT_SYMLINK_FOLLOW
1009    // resolves the /proc/self/fd magic-link to the anon inode.
1010    let r = unsafe {
1011        libc::linkat(
1012            libc::AT_FDCWD,
1013            proc_c.as_ptr(),
1014            libc::AT_FDCWD,
1015            final_c.as_ptr(),
1016            libc::AT_SYMLINK_FOLLOW,
1017        )
1018    };
1019    if r == 0 {
1020        // CAS bytes are read-once into reflinks/hardlinks. Drop them
1021        // from the page cache so the parallel linker pass over many
1022        // packages doesn't push the working set out. Per-file cost is
1023        // roughly fixed regardless of size, so small files paid a
1024        // disproportionate share — gate on `small_threshold` to match
1025        // the fallocate gate above.
1026        if is_large {
1027            use std::os::fd::AsRawFd;
1028            let fd = file.as_raw_fd();
1029            // SAFETY: fd is still owned by `file` here. POSIX_FADV_DONTNEED
1030            // is advisory, return value is ignored.
1031            unsafe {
1032                libc::posix_fadvise(fd, 0, 0, libc::POSIX_FADV_DONTNEED);
1033            }
1034        }
1035        return Ok(CasWriteOutcome::Created);
1036    }
1037    let err = std::io::Error::last_os_error();
1038    match err.raw_os_error() {
1039        Some(libc::EEXIST) => Ok(CasWriteOutcome::AlreadyExisted),
1040        // No /proc in this sandbox.
1041        Some(libc::ENOENT) => Err(OTmpfileFallback::Unsupported),
1042        // Kernel opens O_TMPFILE but rejects linkat from /proc/self/fd.
1043        // ENOTSUP is same value as EOPNOTSUPP on Linux.
1044        Some(libc::EOPNOTSUPP) | Some(libc::EXDEV) => Err(OTmpfileFallback::Unsupported),
1045        // Seccomp-filtered containers (gVisor, strict k8s pod-security
1046        // profiles) block linkat and return EPERM/EACCES. Fall through
1047        // to the tempfile path instead of aborting the install.
1048        Some(libc::EPERM) | Some(libc::EACCES) => Err(OTmpfileFallback::Unsupported),
1049        _ => Err(OTmpfileFallback::Hard(Error::Io(path.to_path_buf(), err))),
1050    }
1051}
1052
1053/// Map a nibble (0–15) to its lowercase hex ASCII byte. Used by
1054/// `ensure_shards_exist` to build the 256 two-character shard names
1055/// without pulling in `format!`/`hex` per call.
1056fn hex_digit(n: u8) -> u8 {
1057    match n {
1058        0..=9 => b'0' + n,
1059        10..=15 => b'a' + n - 10,
1060        _ => unreachable!(),
1061    }
1062}