Skip to main content

capsule_runtime/
pinned_programs.rs

1//! The program store, `~/.murmur/programs/`: the pinned downloads a tool's `programs:` block names
2//! ([`murmur_artifact::parse_program_downloads`]), fetched once by `mur install`, and which copy of
3//! each pinned program a session runs.
4//!
5//! ```text
6//! ~/.murmur/programs/
7//!   <sha256>/              content-addressed by the declared sha256, shared by every artifact
8//!     record.json          PinnedRecord
9//!     files/               the unpacked zip, or the single downloaded file
10//!     bin/<program>        relative symlink to ../files/<run>, one per program name
11//!   .partial-<random>/     an install in flight; never read, removed on any failure
12//! ```
13//!
14//! Every directory is `0700`; every file `0700` when it is executable and `0600` otherwise. An
15//! entry is assembled in a `.partial-*` folder and moved into place by one `rename`, so a reader
16//! sees a whole entry or none.
17//!
18//! The store is written by `mur install` alone ([`commit_download`]). Staging only reads it
19//! ([`resolve_pinned_programs`]): a host program of the same name on `PATH` wins; otherwise a
20//! copy whose `record.json` pins the declared sha256 and whose `bin/<program>` resolves to an
21//! executable file is used, without re-hashing the tree, so launch time does not grow with the
22//! download. `mur install` and `mur doctor` verify the whole tree ([`verify_entry`]).
23
24use std::io::{Read, Write};
25use std::path::{Path, PathBuf};
26
27use murmur_artifact::{LockOrigin, PinnedDownload, ProgramDownloads};
28use serde::{Deserialize, Serialize};
29
30/// The store's name under `~/.murmur`.
31pub const PROGRAMS_DIR: &str = "programs";
32
33/// An entry's record of what was fetched.
34pub const RECORD_FILE: &str = "record.json";
35
36/// The folder of an entry holding the downloaded program.
37pub const FILES_DIR: &str = "files";
38
39/// The folder of an entry holding one link per program name, appended to a session's `PATH`.
40pub const BIN_DIR: &str = "bin";
41
42/// The name prefix of an install in flight.
43pub const PARTIAL_PREFIX: &str = ".partial-";
44
45/// The most bytes a download, and the tree unpacked from it, may hold: 2 GiB.
46pub const MAX_PINNED_DOWNLOAD_BYTES: u64 = 2 * 1024 * 1024 * 1024;
47
48/// The mode of an executable file in the store; every other file is
49/// [`crate::murmur_home::PRIVATE_FILE_MODE`] and every directory
50/// [`crate::murmur_home::MURMUR_HOME_DIR_MODE`].
51pub const EXECUTABLE_FILE_MODE: u32 = 0o700;
52
53/// The file a download streams into inside its `.partial-*` folder.
54const DOWNLOAD_FILE: &str = "download";
55
56/// `~/.murmur/programs`, whether or not it exists. Creates nothing.
57pub fn program_store_root() -> Result<PathBuf, String> {
58    Ok(crate::state_store::murmur_home_dir()?.join(PROGRAMS_DIR))
59}
60
61/// `~/.murmur/programs`, created owner-only (and `~/.murmur` with it) when missing.
62pub fn ensure_program_store() -> Result<PathBuf, String> {
63    let root = crate::murmur_home::ensure_murmur_home()?.join(PROGRAMS_DIR);
64    crate::murmur_home::hold_private_dir(&root)
65        .map_err(|reason| format!("{}: {reason}", root.display()))?;
66    Ok(root)
67}
68
69/// `record.json`: what an entry was fetched from, and the digest of what it holds.
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct PinnedRecord {
72    pub url: String,
73    /// The SHA-256 of the downloaded bytes, the entry's folder name.
74    pub sha256: String,
75    pub platform: String,
76    /// The file the program runs, relative to `files/`, for a zip; `None` for a single file.
77    pub run: Option<String>,
78    /// The SHA-256 of [`Self::files`] rendered as sorted `<path>\t<x|->\t<sha256>\n` lines.
79    pub tree_sha256: String,
80    /// When the entry was committed, RFC 3339 UTC.
81    pub fetched_at: String,
82    /// Every regular file under `files/`, sorted by path.
83    pub files: Vec<TreeFile>,
84}
85
86impl PinnedRecord {
87    /// The program file, relative to `files/`: `run`, or the single downloaded file.
88    pub fn program_file(&self) -> Option<&str> {
89        match &self.run {
90            Some(run) => Some(run.as_str()),
91            None => self.files.first().map(|file| file.path.as_str()),
92        }
93    }
94}
95
96/// One regular file under an entry's `files/`.
97#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
98pub struct TreeFile {
99    /// Relative to `files/`, `/`-separated.
100    pub path: String,
101    /// Whether the owner execute bit is set.
102    pub executable: bool,
103    pub sha256: String,
104}
105
106/// The SHA-256 over `files` as sorted `<path>\t<x|->\t<sha256>\n` lines.
107pub fn tree_digest(files: &[TreeFile]) -> String {
108    let mut sorted: Vec<&TreeFile> = files.iter().collect();
109    sorted.sort();
110    let lines: String = sorted
111        .into_iter()
112        .map(|file| {
113            let mark = if file.executable { "x" } else { "-" };
114            format!("{}\t{mark}\t{}\n", file.path, file.sha256)
115        })
116        .collect();
117    murmur_artifact::sha256_hex(lines.as_bytes())
118}
119
120/// Why committing a download failed.
121#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
122pub enum PinnedStoreError {
123    /// The download is not a tree the store takes: a missing `run`, a symlink or escaping entry, a
124    /// tree over [`MAX_PINNED_DOWNLOAD_BYTES`], or bytes that are no zip.
125    #[error("{0}")]
126    Unpack(String),
127    /// The store could not be written.
128    #[error("{0}")]
129    Store(String),
130}
131
132/// An install in flight: `~/.murmur/programs/.partial-<random>/`, removed when dropped
133/// uncommitted.
134#[derive(Debug)]
135pub struct PartialDownload {
136    dir: PathBuf,
137    kept: bool,
138}
139
140impl PartialDownload {
141    /// Creates a fresh owner-only `.partial-*` folder under `store_root`.
142    pub fn begin(store_root: &Path) -> Result<Self, PinnedStoreError> {
143        let mut random = [0u8; 8];
144        getrandom::fill(&mut random)
145            .map_err(|err| PinnedStoreError::Store(format!("no randomness for a name: {err}")))?;
146        let dir = store_root.join(format!(
147            "{PARTIAL_PREFIX}{:016x}",
148            u64::from_be_bytes(random)
149        ));
150        create_private_dir(&dir)?;
151        Ok(Self { dir, kept: false })
152    }
153
154    /// The file the download streams into.
155    pub fn download_path(&self) -> PathBuf {
156        self.dir.join(DOWNLOAD_FILE)
157    }
158
159    /// Opens [`Self::download_path`] for writing, owner-only.
160    pub fn create_download_file(&self) -> Result<std::fs::File, PinnedStoreError> {
161        use std::os::unix::fs::OpenOptionsExt;
162        std::fs::OpenOptions::new()
163            .write(true)
164            .create_new(true)
165            .mode(crate::murmur_home::PRIVATE_FILE_MODE)
166            .open(self.download_path())
167            .map_err(|err| store_error(&self.download_path(), err))
168    }
169}
170
171impl Drop for PartialDownload {
172    fn drop(&mut self) {
173        if !self.kept {
174            let _ = std::fs::remove_dir_all(&self.dir);
175        }
176    }
177}
178
179/// What [`commit_download`] left in the store.
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct CommittedEntry {
182    /// `<store_root>/<sha256>`.
183    pub entry: PathBuf,
184    /// The program file, canonical.
185    pub program_path: PathBuf,
186    /// `true` when a concurrent install stored the entry first and ours was discarded.
187    pub raced: bool,
188}
189
190/// Unpacks a download whose bytes already hash to `download.sha256` and moves it into
191/// `<store_root>/<sha256>/` with its record and `bin/<program>` link.
192///
193/// `partial` holds the downloaded bytes at [`PartialDownload::download_path`]. With `run`, they
194/// are a zip: every entry is unpacked under `files/` with group and other bits stripped; a
195/// symlink entry, an absolute or `..` entry name, a tree over [`MAX_PINNED_DOWNLOAD_BYTES`] or a
196/// `run` naming no regular file refuses ([`PinnedStoreError::Unpack`]). Without `run`, the bytes
197/// are the program, stored executable under the URL's last path segment. The record is written
198/// last and the folder renamed into place; when a concurrent install got there first, ours is
199/// discarded and theirs verified. Nothing is left behind on any failure.
200pub fn commit_download(
201    store_root: &Path,
202    download: &PinnedDownload,
203    program: &str,
204    partial: PartialDownload,
205) -> Result<CommittedEntry, PinnedStoreError> {
206    commit_download_capped(
207        store_root,
208        download,
209        program,
210        partial,
211        MAX_PINNED_DOWNLOAD_BYTES,
212    )
213}
214
215pub(crate) fn commit_download_capped(
216    store_root: &Path,
217    download: &PinnedDownload,
218    program: &str,
219    mut partial: PartialDownload,
220    cap: u64,
221) -> Result<CommittedEntry, PinnedStoreError> {
222    let entry = entry_path(store_root, &download.sha256)?;
223    let files = partial.dir.join(FILES_DIR);
224    create_private_dir(&files)?;
225    let downloaded = partial.download_path();
226    let program_file = match &download.run {
227        Some(run) => {
228            unpack_zip(&downloaded, &files, cap)?;
229            let target = files.join(run);
230            match std::fs::symlink_metadata(&target) {
231                Ok(metadata) if metadata.is_file() => {}
232                Ok(_) => {
233                    return Err(PinnedStoreError::Unpack(format!(
234                        "run names {run}, which is not a regular file in the zip"
235                    )))
236                }
237                Err(_) => {
238                    return Err(PinnedStoreError::Unpack(format!(
239                        "run names {run}, and the zip holds no such file"
240                    )))
241                }
242            }
243            std::fs::remove_file(&downloaded).map_err(|err| store_error(&downloaded, err))?;
244            run.clone()
245        }
246        None => {
247            let size = std::fs::metadata(&downloaded)
248                .map_err(|err| store_error(&downloaded, err))?
249                .len();
250            if size > cap {
251                return Err(PinnedStoreError::Unpack(format!(
252                    "the download is {size} bytes, over the {cap}-byte cap"
253                )));
254            }
255            let name = single_file_name(&download.url);
256            let target = files.join(&name);
257            std::fs::rename(&downloaded, &target).map_err(|err| store_error(&target, err))?;
258            set_mode(&target, EXECUTABLE_FILE_MODE)?;
259            name
260        }
261    };
262    let tree = list_tree(&files).map_err(PinnedStoreError::Store)?;
263    link_program(&partial.dir, program, &program_file)?;
264    let record = PinnedRecord {
265        url: download.url.clone(),
266        sha256: download.sha256.clone(),
267        platform: download.platform.clone(),
268        run: download.run.clone(),
269        tree_sha256: tree_digest(&tree),
270        fetched_at: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
271        files: tree,
272    };
273    write_record(&partial.dir, &record)?;
274
275    match std::fs::rename(&partial.dir, &entry) {
276        Ok(()) => {
277            partial.kept = true;
278            let program_path = std::fs::canonicalize(entry.join(FILES_DIR).join(&program_file))
279                .map_err(|err| store_error(&entry, err))?;
280            Ok(CommittedEntry {
281                entry,
282                program_path,
283                raced: false,
284            })
285        }
286        Err(err) if entry.is_dir() => {
287            drop(partial);
288            match verify_entry(store_root, download) {
289                EntryState::Present { path, .. } => {
290                    ensure_bin_link(store_root, download, program)?;
291                    Ok(CommittedEntry {
292                        entry,
293                        program_path: path,
294                        raced: true,
295                    })
296                }
297                EntryState::Missing => Err(store_error(&entry, err)),
298                EntryState::ChecksumFailed { reason } => Err(PinnedStoreError::Store(format!(
299                    "{} was stored by another install and fails its checksum: {reason}",
300                    entry.display()
301                ))),
302            }
303        }
304        Err(err) => Err(store_error(&entry, err)),
305    }
306}
307
308/// What the store holds for one download.
309#[derive(Debug, Clone, PartialEq, Eq)]
310pub enum EntryState {
311    /// The tree verifies against its record and the record pins the declared sha256.
312    Present {
313        /// The program file, canonical.
314        path: PathBuf,
315        record: PinnedRecord,
316    },
317    /// No entry for the declared sha256.
318    Missing,
319    /// An entry that fails verification, and why: a file changed, missing or added since install,
320    /// or an unreadable record.
321    ChecksumFailed { reason: String },
322}
323
324/// Verifies the entry for `download.sha256` in full: reads its record, re-hashes every file under
325/// `files/` and compares. Reads only.
326pub fn verify_entry(store_root: &Path, download: &PinnedDownload) -> EntryState {
327    let Ok(entry) = entry_path(store_root, &download.sha256) else {
328        return EntryState::Missing;
329    };
330    if std::fs::symlink_metadata(&entry).is_err() {
331        return EntryState::Missing;
332    }
333    let failed = |reason: String| EntryState::ChecksumFailed { reason };
334    let record = match read_record(&entry) {
335        Ok(record) => record,
336        Err(reason) => return failed(reason),
337    };
338    if record.sha256 != download.sha256 {
339        return failed(format!(
340            "{RECORD_FILE} names sha256 {}, not the pinned {}",
341            record.sha256, download.sha256
342        ));
343    }
344    let tree = match list_tree(&entry.join(FILES_DIR)) {
345        Ok(tree) => tree,
346        Err(reason) => return failed(reason),
347    };
348    if let Some(reason) = tree_difference(&record.files, &tree) {
349        return failed(reason);
350    }
351    if tree_digest(&tree) != record.tree_sha256 {
352        return failed(format!(
353            "{RECORD_FILE}'s tree_sha256 does not match the files it lists"
354        ));
355    }
356    let Some(program_file) = record.program_file() else {
357        return failed(format!("{RECORD_FILE} names no program file"));
358    };
359    match std::fs::canonicalize(entry.join(FILES_DIR).join(program_file)) {
360        Ok(path) => EntryState::Present { path, record },
361        Err(_) => failed(format!(
362            "{FILES_DIR}/{program_file} is missing since install"
363        )),
364    }
365}
366
367/// Adds `bin/<program>` to the verified entry for `download` when it is missing or points
368/// elsewhere. `Ok(true)` when a link was written.
369pub fn ensure_bin_link(
370    store_root: &Path,
371    download: &PinnedDownload,
372    program: &str,
373) -> Result<bool, PinnedStoreError> {
374    let entry = entry_path(store_root, &download.sha256)?;
375    let record = read_record(&entry).map_err(PinnedStoreError::Store)?;
376    let program_file = record
377        .program_file()
378        .ok_or_else(|| PinnedStoreError::Store(format!("{RECORD_FILE} names no program file")))?
379        .to_string();
380    let link = entry.join(BIN_DIR).join(program);
381    if std::fs::read_link(&link).ok() == Some(link_target(&program_file)) {
382        return Ok(false);
383    }
384    if std::fs::symlink_metadata(&link).is_ok() {
385        std::fs::remove_file(&link).map_err(|err| store_error(&link, err))?;
386    }
387    link_program(&entry, program, &program_file)?;
388    Ok(true)
389}
390
391/// Removes the entry for `sha256`, whatever state it is in.
392pub fn remove_entry(store_root: &Path, sha256: &str) -> Result<(), PinnedStoreError> {
393    let entry = entry_path(store_root, sha256)?;
394    match std::fs::remove_dir_all(&entry) {
395        Ok(()) => Ok(()),
396        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
397        Err(err) => Err(store_error(&entry, err)),
398    }
399}
400
401/// Reads an entry's `record.json`.
402pub fn read_record(entry: &Path) -> Result<PinnedRecord, String> {
403    let path = entry.join(RECORD_FILE);
404    let text = std::fs::read_to_string(&path)
405        .map_err(|err| format!("{RECORD_FILE} cannot be read: {err}"))?;
406    serde_json::from_str(&text).map_err(|err| format!("{RECORD_FILE} cannot be parsed: {err}"))
407}
408
409/// One staged tool's pinned downloads, with the lock origin of its pin.
410#[derive(Debug, Clone, PartialEq, Eq)]
411pub struct DeclaredToolDownloads {
412    /// The artifact as the capsule's manifest names it, scope included.
413    pub artifact: String,
414    pub origin: LockOrigin,
415    /// Only the programs that pin a download.
416    pub programs: Vec<ProgramDownloads>,
417}
418
419/// Which copy of a pinned program a session runs.
420#[derive(Debug, Clone, PartialEq, Eq)]
421pub enum PinnedSource {
422    /// The store's copy: `bin_dir` joins the end of every command's `PATH`, `tree` is granted
423    /// listable and executable, never writable.
424    Pinned {
425        /// The program file, canonical.
426        path: PathBuf,
427        /// `<tree>/bin`.
428        bin_dir: PathBuf,
429        /// The entry, `~/.murmur/programs/<sha256>`, canonical.
430        tree: PathBuf,
431        url: String,
432        sha256: String,
433    },
434    /// A host program of that name on `PATH`, canonical; it runs as it would with no download.
435    Host { path: PathBuf },
436    /// Nothing runs under that name from the store, and why.
437    Missing { reason: String },
438}
439
440impl PinnedSource {
441    /// `"pinned"`, `"host"` or `"missing"`.
442    pub fn label(&self) -> &'static str {
443        match self {
444            Self::Pinned { .. } => "pinned",
445            Self::Host { .. } => "host",
446            Self::Missing { .. } => "missing",
447        }
448    }
449}
450
451/// One gated program's source choice.
452#[derive(Debug, Clone, PartialEq, Eq)]
453pub struct ResolvedPinnedProgram {
454    pub artifact: String,
455    pub program: String,
456    pub platform: String,
457    pub source: PinnedSource,
458}
459
460impl ResolvedPinnedProgram {
461    /// `(bin_dir, tree)` for a [`PinnedSource::Pinned`] choice.
462    pub fn pinned_dirs(&self) -> Option<(&Path, &Path)> {
463        match &self.source {
464            PinnedSource::Pinned { bin_dir, tree, .. } => Some((bin_dir, tree)),
465            _ => None,
466        }
467    }
468}
469
470/// The `bin/` folder of every [`PinnedSource::Pinned`] choice, distinct, in order.
471pub fn pinned_bin_dirs(programs: &[ResolvedPinnedProgram]) -> Vec<PathBuf> {
472    let mut dirs: Vec<PathBuf> = Vec::new();
473    for (bin_dir, _) in programs
474        .iter()
475        .filter_map(ResolvedPinnedProgram::pinned_dirs)
476    {
477        if !dirs.iter().any(|dir| dir == bin_dir) {
478            dirs.push(bin_dir.to_path_buf());
479        }
480    }
481    dirs
482}
483
484/// The entry of every [`PinnedSource::Pinned`] choice, distinct, in order.
485pub fn pinned_trees(programs: &[ResolvedPinnedProgram]) -> Vec<PathBuf> {
486    let mut trees: Vec<PathBuf> = Vec::new();
487    for (_, tree) in programs
488        .iter()
489        .filter_map(ResolvedPinnedProgram::pinned_dirs)
490    {
491        if !trees.iter().any(|known| known == tree) {
492            trees.push(tree.to_path_buf());
493        }
494    }
495    trees
496}
497
498/// The source of each program `tools` pin a download of for this host, for the programs
499/// `shell_allow` grants, against mur's own `PATH` and `~/.murmur/programs`.
500///
501/// Reads only, never fetches, never fails: a program the store holds no copy of is
502/// [`PinnedSource::Missing`].
503pub fn derive_pinned_programs(
504    tools: &[DeclaredToolDownloads],
505    shell_allow: &[String],
506) -> Vec<ResolvedPinnedProgram> {
507    let store_root = program_store_root();
508    resolve_pinned_programs(
509        tools,
510        shell_allow,
511        &host_path_dirs(),
512        store_root.as_deref().map_err(String::as_str),
513        murmur_artifact::host_download_platform(),
514    )
515}
516
517/// mur's own `PATH`, split.
518pub fn host_path_dirs() -> Vec<PathBuf> {
519    std::env::var_os("PATH")
520        .map(|path| std::env::split_paths(&path).collect())
521        .unwrap_or_default()
522}
523
524/// [`derive_pinned_programs`] with the host injected.
525///
526/// Only a tool whose `origin` is [`LockOrigin::Operator`] contributes, only for a program key
527/// equal to an entry of `shell_allow`, and only when it pins a download for `platform`. One
528/// element per (artifact, program), tools in the order given and programs in declaration order.
529/// Per program: [`PinnedSource::Host`] when `path_dirs` hold an executable of that name; else
530/// [`PinnedSource::Pinned`] when the entry's record pins the declared sha256 and `bin/<program>`
531/// resolves to an executable file inside it; else [`PinnedSource::Missing`].
532pub fn resolve_pinned_programs(
533    tools: &[DeclaredToolDownloads],
534    shell_allow: &[String],
535    path_dirs: &[PathBuf],
536    store_root: Result<&Path, &str>,
537    platform: Option<&str>,
538) -> Vec<ResolvedPinnedProgram> {
539    let Some(platform) = platform else {
540        return Vec::new();
541    };
542    let mut resolved = Vec::new();
543    for (tool, program) in gated_programs(tools, shell_allow) {
544        let Some(download) = program.for_platform(platform) else {
545            continue;
546        };
547        let source = match host_program(&program.program, path_dirs) {
548            Some(path) => PinnedSource::Host { path },
549            None => stored_source(store_root, download, &program.program),
550        };
551        resolved.push(ResolvedPinnedProgram {
552            artifact: tool.artifact.clone(),
553            program: program.program.clone(),
554            platform: platform.to_string(),
555            source,
556        });
557    }
558    resolved
559}
560
561/// The programs of `tools` a session may run a pinned copy of, whatever the platform: only from a
562/// tool whose `origin` is [`LockOrigin::Operator`], and only for a program key equal to an entry
563/// of `shell_allow`. Tools in the order given, programs in declaration order.
564pub fn gated_programs<'a>(
565    tools: &'a [DeclaredToolDownloads],
566    shell_allow: &'a [String],
567) -> impl Iterator<Item = (&'a DeclaredToolDownloads, &'a ProgramDownloads)> + 'a {
568    tools
569        .iter()
570        .filter(|tool| tool.origin == LockOrigin::Operator)
571        .flat_map(move |tool| {
572            tool.programs
573                .iter()
574                .filter(move |program| shell_allow.contains(&program.program))
575                .map(move |program| (tool, program))
576        })
577}
578
579/// The first executable named `program` in `path_dirs`, canonical: the one `execvp` would run.
580pub fn host_program(program: &str, path_dirs: &[PathBuf]) -> Option<PathBuf> {
581    path_dirs
582        .iter()
583        .map(|dir| dir.join(program))
584        .find(|candidate| crate::sandbox::is_executable_file(candidate))
585        .and_then(|candidate| std::fs::canonicalize(candidate).ok())
586}
587
588/// The store's copy for `download`, checked as far as staging checks it: the record and the
589/// link, never the tree's digest.
590fn stored_source(
591    store_root: Result<&Path, &str>,
592    download: &PinnedDownload,
593    program: &str,
594) -> PinnedSource {
595    let missing = |reason: String| PinnedSource::Missing { reason };
596    let sha256 = &download.sha256;
597    let store_root = match store_root {
598        Ok(root) => root,
599        Err(reason) => {
600            return missing(format!("the program store cannot be located: {reason}"));
601        }
602    };
603    let Ok(entry) = entry_path(store_root, sha256) else {
604        return missing(format!("sha256 {sha256} cannot name a store entry"));
605    };
606    let Ok(tree) = std::fs::canonicalize(&entry) else {
607        return missing(format!(
608            "the store holds no copy for sha256 {sha256}; run mur install"
609        ));
610    };
611    let record = match read_record(&tree) {
612        Ok(record) => record,
613        Err(reason) => {
614            return missing(format!(
615                "the store's copy for sha256 {sha256} has no usable record ({reason}); run mur \
616                 install"
617            ))
618        }
619    };
620    if record.sha256 != *sha256 {
621        return missing(format!(
622            "the store's record for sha256 {sha256} names sha256 {}; run mur install",
623            record.sha256
624        ));
625    }
626    let link = tree.join(BIN_DIR).join(program);
627    let runnable = std::fs::canonicalize(&link)
628        .ok()
629        .filter(|path| path.starts_with(tree.join(FILES_DIR)))
630        .filter(|path| crate::sandbox::is_executable_file(path));
631    match runnable {
632        Some(path) => PinnedSource::Pinned {
633            path,
634            bin_dir: tree.join(BIN_DIR),
635            tree,
636            url: record.url,
637            sha256: sha256.clone(),
638        },
639        None => missing(format!(
640            "the store's copy for sha256 {sha256} has no runnable {BIN_DIR}/{program}; run mur \
641             install"
642        )),
643    }
644}
645
646/// `<store_root>/<sha256>`, refusing a `sha256` that is not 64 lowercase hex digits.
647fn entry_path(store_root: &Path, sha256: &str) -> Result<PathBuf, PinnedStoreError> {
648    if !murmur_artifact::is_sha256_hex(sha256) {
649        return Err(PinnedStoreError::Store(format!(
650            "'{sha256}' is not a sha256 the store names an entry by"
651        )));
652    }
653    Ok(store_root.join(sha256))
654}
655
656/// The file name a single-file download is stored under: the URL's last path segment, or
657/// `download` when it has none usable.
658fn single_file_name(url: &str) -> String {
659    url::Url::parse(url)
660        .ok()
661        .and_then(|url| {
662            url.path_segments()
663                .and_then(|mut segments| segments.next_back().map(str::to_string))
664        })
665        .filter(|name| {
666            !name.is_empty() && name != "." && name != ".." && !name.contains(['/', '\\', '\0'])
667        })
668        .unwrap_or_else(|| DOWNLOAD_FILE.to_string())
669}
670
671fn link_target(program_file: &str) -> PathBuf {
672    Path::new("..").join(FILES_DIR).join(program_file)
673}
674
675/// Writes `<entry>/bin/<program>` -> `../files/<program_file>`.
676fn link_program(entry: &Path, program: &str, program_file: &str) -> Result<(), PinnedStoreError> {
677    let bin = entry.join(BIN_DIR);
678    if !bin.is_dir() {
679        create_private_dir(&bin)?;
680    }
681    let link = bin.join(program);
682    std::os::unix::fs::symlink(link_target(program_file), &link)
683        .map_err(|err| store_error(&link, err))
684}
685
686fn write_record(entry: &Path, record: &PinnedRecord) -> Result<(), PinnedStoreError> {
687    use std::os::unix::fs::OpenOptionsExt;
688    let path = entry.join(RECORD_FILE);
689    let text = serde_json::to_string_pretty(record)
690        .map_err(|err| PinnedStoreError::Store(format!("{RECORD_FILE}: {err}")))?;
691    let mut file = std::fs::OpenOptions::new()
692        .write(true)
693        .create_new(true)
694        .mode(crate::murmur_home::PRIVATE_FILE_MODE)
695        .open(&path)
696        .map_err(|err| store_error(&path, err))?;
697    file.write_all(text.as_bytes())
698        .and_then(|()| file.sync_all())
699        .map_err(|err| store_error(&path, err))
700}
701
702/// Unpacks the zip at `zip_path` under `into`, refusing what the store does not take.
703fn unpack_zip(zip_path: &Path, into: &Path, cap: u64) -> Result<(), PinnedStoreError> {
704    let unpack = PinnedStoreError::Unpack;
705    let file = std::fs::File::open(zip_path).map_err(|err| store_error(zip_path, err))?;
706    let mut archive = zip::ZipArchive::new(file)
707        .map_err(|err| unpack(format!("the download is no zip: {err}")))?;
708    let mut total: u64 = 0;
709    for index in 0..archive.len() {
710        let mut entry = archive
711            .by_index(index)
712            .map_err(|err| unpack(format!("zip entry {index} cannot be read: {err}")))?;
713        let name = entry
714            .name()
715            .map_err(|err| unpack(format!("zip entry {index} has no readable name: {err}")))?
716            .to_string();
717        if entry.is_symlink() {
718            return Err(unpack(format!(
719                "zip entry '{name}' is a symlink; a pinned download unpacks only files and folders"
720            )));
721        }
722        if name.starts_with('/') || name.contains('\\') {
723            return Err(unpack(format!(
724                "zip entry '{name}' escapes the archive's folder"
725            )));
726        }
727        let relative = murmur_artifact::zip_guard::normalized_entry_name(&name)
728            .map_err(|_| unpack(format!("zip entry '{name}' escapes the archive's folder")))?;
729        if relative.is_empty() {
730            continue;
731        }
732        let target = into.join(&relative);
733        if entry.is_dir() {
734            create_private_dirs(into, &target)?;
735            continue;
736        }
737        if let Some(parent) = target.parent() {
738            create_private_dirs(into, parent)?;
739        }
740        let executable = entry.unix_mode().is_some_and(|mode| mode & 0o111 != 0);
741        let mode = if executable {
742            EXECUTABLE_FILE_MODE
743        } else {
744            crate::murmur_home::PRIVATE_FILE_MODE
745        };
746        let mut out = {
747            use std::os::unix::fs::OpenOptionsExt;
748            std::fs::OpenOptions::new()
749                .write(true)
750                .create_new(true)
751                .mode(mode)
752                .open(&target)
753                .map_err(|err| match err.kind() {
754                    std::io::ErrorKind::AlreadyExists => {
755                        unpack(format!("zip entry '{name}' appears more than once"))
756                    }
757                    _ => store_error(&target, err),
758                })?
759        };
760        let remaining = cap.saturating_sub(total);
761        let written = std::io::copy(&mut (&mut entry).take(remaining + 1), &mut out)
762            .map_err(|err| unpack(format!("zip entry '{name}' cannot be unpacked: {err}")))?;
763        total += written;
764        if total > cap {
765            return Err(unpack(format!(
766                "the unpacked tree is over the {cap}-byte cap"
767            )));
768        }
769        set_mode(&target, mode)?;
770    }
771    Ok(())
772}
773
774/// Creates `dir` and every missing folder between `root` and it, each owner-only.
775fn create_private_dirs(root: &Path, dir: &Path) -> Result<(), PinnedStoreError> {
776    let Ok(relative) = dir.strip_prefix(root) else {
777        return Err(PinnedStoreError::Unpack(format!(
778            "{} escapes the archive's folder",
779            dir.display()
780        )));
781    };
782    let mut current = root.to_path_buf();
783    for component in relative.components() {
784        current.push(component);
785        match std::fs::symlink_metadata(&current) {
786            Ok(metadata) if metadata.is_dir() => {}
787            Ok(_) => {
788                return Err(PinnedStoreError::Unpack(format!(
789                    "zip entry folder {} is also a file",
790                    current.display()
791                )))
792            }
793            Err(_) => create_private_dir(&current)?,
794        }
795    }
796    Ok(())
797}
798
799fn create_private_dir(dir: &Path) -> Result<(), PinnedStoreError> {
800    use std::os::unix::fs::DirBuilderExt;
801    std::fs::DirBuilder::new()
802        .mode(crate::murmur_home::MURMUR_HOME_DIR_MODE)
803        .create(dir)
804        .map_err(|err| store_error(dir, err))?;
805    set_mode(dir, crate::murmur_home::MURMUR_HOME_DIR_MODE)
806}
807
808fn set_mode(path: &Path, mode: u32) -> Result<(), PinnedStoreError> {
809    use std::os::unix::fs::PermissionsExt;
810    std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
811        .map_err(|err| store_error(path, err))
812}
813
814/// Every regular file under `files`, sorted by path; refuses anything but files and folders.
815fn list_tree(files: &Path) -> Result<Vec<TreeFile>, String> {
816    let mut found = Vec::new();
817    walk(files, files, &mut found)?;
818    found.sort();
819    Ok(found)
820}
821
822fn walk(root: &Path, dir: &Path, found: &mut Vec<TreeFile>) -> Result<(), String> {
823    use std::os::unix::fs::PermissionsExt;
824    let entries = std::fs::read_dir(dir)
825        .map_err(|err| format!("{} cannot be read: {err}", shown(root, dir)))?;
826    for entry in entries {
827        let path = entry.map_err(|err| err.to_string())?.path();
828        let metadata = std::fs::symlink_metadata(&path).map_err(|err| err.to_string())?;
829        if metadata.is_dir() {
830            walk(root, &path, found)?;
831        } else if metadata.is_file() {
832            found.push(TreeFile {
833                path: relative_name(root, &path),
834                executable: metadata.permissions().mode() & 0o100 != 0,
835                sha256: file_sha256(&path)
836                    .map_err(|err| format!("{} cannot be read: {err}", shown(root, &path)))?,
837            });
838        } else {
839            return Err(format!(
840                "{} is not a regular file or folder",
841                shown(root, &path)
842            ));
843        }
844    }
845    Ok(())
846}
847
848/// The first difference between the files a record lists and the files on disk.
849fn tree_difference(recorded: &[TreeFile], actual: &[TreeFile]) -> Option<String> {
850    for file in recorded {
851        match actual.iter().find(|other| other.path == file.path) {
852            None => {
853                return Some(format!(
854                    "{FILES_DIR}/{} is missing since install",
855                    file.path
856                ))
857            }
858            Some(other) if other != file => {
859                return Some(format!("{FILES_DIR}/{} changed since install", file.path))
860            }
861            Some(_) => {}
862        }
863    }
864    actual
865        .iter()
866        .find(|file| !recorded.iter().any(|other| other.path == file.path))
867        .map(|file| format!("{FILES_DIR}/{} was added since install", file.path))
868}
869
870fn relative_name(root: &Path, path: &Path) -> String {
871    path.strip_prefix(root)
872        .unwrap_or(path)
873        .components()
874        .map(|component| component.as_os_str().to_string_lossy().into_owned())
875        .collect::<Vec<_>>()
876        .join("/")
877}
878
879fn shown(root: &Path, path: &Path) -> String {
880    let relative = relative_name(root, path);
881    if relative.is_empty() {
882        FILES_DIR.to_string()
883    } else {
884        format!("{FILES_DIR}/{relative}")
885    }
886}
887
888/// The SHA-256 of the file at `path`, lowercase hex, read in chunks.
889fn file_sha256(path: &Path) -> std::io::Result<String> {
890    let mut file = std::fs::File::open(path)?;
891    murmur_artifact::sha256_hex_of_reader(&mut file).map(|(_, digest)| digest)
892}
893
894fn store_error(path: &Path, err: std::io::Error) -> PinnedStoreError {
895    PinnedStoreError::Store(format!("{}: {err}", path.display()))
896}
897
898#[cfg(test)]
899mod tests {
900    use super::*;
901    use std::os::unix::fs::PermissionsExt;
902
903    const PROGRAM: &str = "fixture-prog";
904
905    /// A zip of `entries`: (name, unix mode, contents); a mode with `0o120000` is a symlink.
906    fn zip_of(entries: &[(&str, u32, &[u8])]) -> Vec<u8> {
907        let mut writer = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
908        for (name, mode, contents) in entries {
909            if mode & 0o170000 == 0o120000 {
910                writer
911                    .add_symlink(
912                        *name,
913                        std::str::from_utf8(contents).unwrap(),
914                        zip::write::SimpleFileOptions::default(),
915                    )
916                    .unwrap();
917                continue;
918            }
919            let options = zip::write::SimpleFileOptions::default().unix_permissions(*mode);
920            writer.start_file(*name, options).unwrap();
921            writer.write_all(contents).unwrap();
922        }
923        writer.finish().unwrap().into_inner()
924    }
925
926    fn fixture_zip() -> Vec<u8> {
927        zip_of(&[
928            (
929                "fixture/fixture-prog",
930                0o755,
931                b"#!/bin/sh\necho pinned-copy-ran\n",
932            ),
933            ("fixture/data.txt", 0o644, b"data\n"),
934        ])
935    }
936
937    fn sha_of(bytes: &[u8]) -> String {
938        murmur_artifact::sha256_hex(bytes)
939    }
940
941    fn download_for(bytes: &[u8], run: Option<&str>) -> PinnedDownload {
942        PinnedDownload {
943            platform: "linux-x86_64".to_string(),
944            url: "http://127.0.0.1:1/fixture-prog.zip".to_string(),
945            sha256: sha_of(bytes),
946            run: run.map(str::to_string),
947        }
948    }
949
950    fn partial_with(store: &Path, bytes: &[u8]) -> PartialDownload {
951        let partial = PartialDownload::begin(store).unwrap();
952        partial
953            .create_download_file()
954            .unwrap()
955            .write_all(bytes)
956            .unwrap();
957        partial
958    }
959
960    fn commit(
961        store: &Path,
962        bytes: &[u8],
963        run: Option<&str>,
964    ) -> Result<CommittedEntry, PinnedStoreError> {
965        commit_download(
966            store,
967            &download_for(bytes, run),
968            PROGRAM,
969            partial_with(store, bytes),
970        )
971    }
972
973    fn mode_of(path: &Path) -> u32 {
974        std::fs::symlink_metadata(path)
975            .unwrap()
976            .permissions()
977            .mode()
978            & 0o777
979    }
980
981    fn no_partials(store: &Path) -> bool {
982        std::fs::read_dir(store).unwrap().all(|entry| {
983            !entry
984                .unwrap()
985                .file_name()
986                .to_string_lossy()
987                .starts_with(PARTIAL_PREFIX)
988        })
989    }
990
991    #[test]
992    fn pinned_programs_commit_unpacks_links_records_and_strips_modes() {
993        let dir = tempfile::tempdir().unwrap();
994        let store = dir.path();
995        let bytes = fixture_zip();
996        let committed = commit(store, &bytes, Some("fixture/fixture-prog")).unwrap();
997        let entry = store.join(sha_of(&bytes));
998        assert_eq!(committed.entry, entry);
999        assert!(!committed.raced);
1000        assert_eq!(
1001            committed.program_path,
1002            std::fs::canonicalize(entry.join("files/fixture/fixture-prog")).unwrap()
1003        );
1004        assert_eq!(mode_of(&entry), 0o700);
1005        assert_eq!(mode_of(&entry.join("files")), 0o700);
1006        assert_eq!(mode_of(&entry.join("files/fixture")), 0o700);
1007        assert_eq!(mode_of(&entry.join("files/fixture/fixture-prog")), 0o700);
1008        assert_eq!(mode_of(&entry.join("files/fixture/data.txt")), 0o600);
1009        assert_eq!(mode_of(&entry.join("record.json")), 0o600);
1010        assert!(!entry.join(DOWNLOAD_FILE).exists());
1011        assert_eq!(
1012            std::fs::read_link(entry.join("bin/fixture-prog")).unwrap(),
1013            PathBuf::from("../files/fixture/fixture-prog")
1014        );
1015        let record = read_record(&entry).unwrap();
1016        assert_eq!(record.sha256, sha_of(&bytes));
1017        assert_eq!(record.run.as_deref(), Some("fixture/fixture-prog"));
1018        assert_eq!(record.platform, "linux-x86_64");
1019        assert_eq!(record.files.len(), 2);
1020        assert_eq!(record.tree_sha256, tree_digest(&record.files));
1021        let lines = format!(
1022            "fixture/data.txt\t-\t{}\nfixture/fixture-prog\tx\t{}\n",
1023            sha_of(b"data\n"),
1024            sha_of(b"#!/bin/sh\necho pinned-copy-ran\n")
1025        );
1026        assert_eq!(record.tree_sha256, sha_of(lines.as_bytes()));
1027        assert!(no_partials(store));
1028        assert!(matches!(
1029            verify_entry(store, &download_for(&bytes, Some("fixture/fixture-prog"))),
1030            EntryState::Present { .. }
1031        ));
1032    }
1033
1034    #[test]
1035    fn pinned_programs_a_single_file_is_stored_executable_under_the_url_name() {
1036        let dir = tempfile::tempdir().unwrap();
1037        let store = dir.path();
1038        let bytes = b"#!/bin/sh\necho single\n";
1039        let committed = commit(store, bytes, None).unwrap();
1040        let entry = store.join(sha_of(bytes));
1041        assert_eq!(
1042            committed.program_path,
1043            std::fs::canonicalize(entry.join("files/fixture-prog.zip")).unwrap()
1044        );
1045        assert_eq!(mode_of(&entry.join("files/fixture-prog.zip")), 0o700);
1046        assert_eq!(
1047            std::fs::read_link(entry.join("bin/fixture-prog")).unwrap(),
1048            PathBuf::from("../files/fixture-prog.zip")
1049        );
1050        assert_eq!(single_file_name("https://h.example/a/b/tool?x=1"), "tool");
1051        assert_eq!(single_file_name("https://h.example/"), "download");
1052    }
1053
1054    #[test]
1055    fn pinned_programs_commit_refuses_a_symlink_an_escape_a_missing_run_and_the_cap() {
1056        let dir = tempfile::tempdir().unwrap();
1057        let store = dir.path();
1058        let cases: Vec<(Vec<u8>, &str, &str)> = vec![
1059            (
1060                zip_of(&[
1061                    ("fixture/fixture-prog", 0o755, b"x"),
1062                    ("fixture/link", 0o120777, b"/etc/passwd"),
1063                ]),
1064                "fixture/fixture-prog",
1065                "zip entry 'fixture/link' is a symlink",
1066            ),
1067            (
1068                zip_of(&[
1069                    ("../escape", 0o644, b"x"),
1070                    ("fixture/fixture-prog", 0o755, b"x"),
1071                ]),
1072                "fixture/fixture-prog",
1073                "zip entry '../escape' escapes the archive's folder",
1074            ),
1075            (
1076                zip_of(&[("fixture/fixture-prog", 0o755, b"x")]),
1077                "fixture/absent",
1078                "run names fixture/absent, and the zip holds no such file",
1079            ),
1080            (
1081                zip_of(&[("fixture/fixture-prog/", 0o755, b"")]),
1082                "fixture/fixture-prog",
1083                "run names fixture/fixture-prog, which is not a regular file in the zip",
1084            ),
1085            (
1086                zip_of(&[
1087                    ("fixture/fixture-prog", 0o755, b"x"),
1088                    ("./fixture/fixture-prog", 0o755, b"y"),
1089                ]),
1090                "fixture/fixture-prog",
1091                "zip entry './fixture/fixture-prog' appears more than once",
1092            ),
1093            (
1094                b"not a zip".to_vec(),
1095                "fixture/fixture-prog",
1096                "the download is no zip",
1097            ),
1098        ];
1099        for (bytes, run, expected) in cases {
1100            let err = commit(store, &bytes, Some(run)).unwrap_err();
1101            assert!(matches!(err, PinnedStoreError::Unpack(_)), "{err:?}");
1102            assert!(err.to_string().contains(expected), "{err}");
1103            assert!(!store.join(sha_of(&bytes)).exists());
1104            assert!(no_partials(store));
1105        }
1106        let bytes = zip_of(&[("fixture/fixture-prog", 0o755, &[7u8; 64])]);
1107        let err = commit_download_capped(
1108            store,
1109            &download_for(&bytes, Some("fixture/fixture-prog")),
1110            PROGRAM,
1111            partial_with(store, &bytes),
1112            32,
1113        )
1114        .unwrap_err();
1115        assert_eq!(
1116            err,
1117            PinnedStoreError::Unpack("the unpacked tree is over the 32-byte cap".to_string())
1118        );
1119        assert!(no_partials(store));
1120    }
1121
1122    #[test]
1123    fn pinned_programs_verify_names_a_changed_missing_or_added_file() {
1124        let dir = tempfile::tempdir().unwrap();
1125        let store = dir.path();
1126        let bytes = fixture_zip();
1127        let download = download_for(&bytes, Some("fixture/fixture-prog"));
1128        assert_eq!(verify_entry(store, &download), EntryState::Missing);
1129        commit(store, &bytes, Some("fixture/fixture-prog")).unwrap();
1130        let entry = store.join(&download.sha256);
1131
1132        std::fs::write(entry.join("files/fixture/data.txt"), "changed\n").unwrap();
1133        assert_eq!(
1134            verify_entry(store, &download),
1135            EntryState::ChecksumFailed {
1136                reason: "files/fixture/data.txt changed since install".to_string()
1137            }
1138        );
1139        std::fs::write(entry.join("files/fixture/data.txt"), "data\n").unwrap();
1140        assert!(matches!(
1141            verify_entry(store, &download),
1142            EntryState::Present { .. }
1143        ));
1144
1145        std::fs::set_permissions(
1146            entry.join("files/fixture/data.txt"),
1147            std::fs::Permissions::from_mode(0o700),
1148        )
1149        .unwrap();
1150        assert_eq!(
1151            verify_entry(store, &download),
1152            EntryState::ChecksumFailed {
1153                reason: "files/fixture/data.txt changed since install".to_string()
1154            }
1155        );
1156        std::fs::remove_file(entry.join("files/fixture/data.txt")).unwrap();
1157        assert_eq!(
1158            verify_entry(store, &download),
1159            EntryState::ChecksumFailed {
1160                reason: "files/fixture/data.txt is missing since install".to_string()
1161            }
1162        );
1163        std::fs::write(entry.join("files/fixture/data.txt"), "data\n").unwrap();
1164        std::fs::set_permissions(
1165            entry.join("files/fixture/data.txt"),
1166            std::fs::Permissions::from_mode(0o600),
1167        )
1168        .unwrap();
1169        std::fs::write(entry.join("files/extra"), "x").unwrap();
1170        assert_eq!(
1171            verify_entry(store, &download),
1172            EntryState::ChecksumFailed {
1173                reason: "files/extra was added since install".to_string()
1174            }
1175        );
1176        std::fs::remove_file(entry.join("files/extra")).unwrap();
1177        std::fs::write(entry.join("record.json"), "{").unwrap();
1178        assert!(matches!(
1179            verify_entry(store, &download),
1180            EntryState::ChecksumFailed { reason } if reason.starts_with("record.json cannot be parsed")
1181        ));
1182    }
1183
1184    #[test]
1185    fn pinned_programs_a_concurrent_commit_keeps_theirs_and_links_ours() {
1186        let dir = tempfile::tempdir().unwrap();
1187        let store = dir.path();
1188        let bytes = fixture_zip();
1189        let download = download_for(&bytes, Some("fixture/fixture-prog"));
1190        let ours = partial_with(store, &bytes);
1191        commit(store, &bytes, Some("fixture/fixture-prog")).unwrap();
1192        let committed = commit_download(store, &download, "other-name", ours).unwrap();
1193        assert!(committed.raced);
1194        let entry = store.join(&download.sha256);
1195        assert_eq!(
1196            std::fs::read_link(entry.join("bin/other-name")).unwrap(),
1197            PathBuf::from("../files/fixture/fixture-prog")
1198        );
1199        assert!(no_partials(store));
1200        assert!(!ensure_bin_link(store, &download, "other-name").unwrap());
1201        std::fs::remove_file(entry.join("bin/fixture-prog")).unwrap();
1202        assert!(ensure_bin_link(store, &download, PROGRAM).unwrap());
1203        remove_entry(store, &download.sha256).unwrap();
1204        assert!(!entry.exists());
1205        remove_entry(store, &download.sha256).unwrap();
1206        assert!(remove_entry(store, "../x").is_err());
1207    }
1208
1209    fn tool(origin: LockOrigin, download: &PinnedDownload, program: &str) -> DeclaredToolDownloads {
1210        DeclaredToolDownloads {
1211            artifact: "local/pinned-tool".to_string(),
1212            origin,
1213            programs: vec![ProgramDownloads {
1214                program: program.to_string(),
1215                downloads: vec![download.clone()],
1216            }],
1217        }
1218    }
1219
1220    fn allow(names: &[&str]) -> Vec<String> {
1221        names.iter().map(|name| (*name).to_string()).collect()
1222    }
1223
1224    #[test]
1225    fn pinned_programs_resolve_follows_the_gate_host_first_then_the_store() {
1226        let dir = tempfile::tempdir().unwrap();
1227        let store = dir.path().join("programs");
1228        std::fs::create_dir(&store).unwrap();
1229        let bytes = fixture_zip();
1230        let download = download_for(&bytes, Some("fixture/fixture-prog"));
1231        let empty_path: Vec<PathBuf> = vec![dir.path().join("no-such-dir")];
1232        let resolve = |tools: &[DeclaredToolDownloads], allowed: &[&str], path: &[PathBuf]| {
1233            resolve_pinned_programs(
1234                tools,
1235                &allow(allowed),
1236                path,
1237                Ok(&store),
1238                Some("linux-x86_64"),
1239            )
1240        };
1241        let operator = [tool(LockOrigin::Operator, &download, PROGRAM)];
1242
1243        // Nothing stored: missing, naming the sha and `mur install`.
1244        let missing = resolve(&operator, &["bash", PROGRAM], &empty_path);
1245        assert_eq!(missing.len(), 1);
1246        assert_eq!(
1247            missing[0].source,
1248            PinnedSource::Missing {
1249                reason: format!(
1250                    "the store holds no copy for sha256 {}; run mur install",
1251                    download.sha256
1252                )
1253            }
1254        );
1255
1256        commit(&store, &bytes, Some("fixture/fixture-prog")).unwrap();
1257        let tree = std::fs::canonicalize(store.join(&download.sha256)).unwrap();
1258        let pinned = resolve(&operator, &[PROGRAM], &empty_path);
1259        assert_eq!(
1260            pinned[0].source,
1261            PinnedSource::Pinned {
1262                path: tree.join("files/fixture/fixture-prog"),
1263                bin_dir: tree.join("bin"),
1264                tree: tree.clone(),
1265                url: download.url.clone(),
1266                sha256: download.sha256.clone(),
1267            }
1268        );
1269        assert_eq!(pinned_bin_dirs(&pinned), vec![tree.join("bin")]);
1270        assert_eq!(pinned_trees(&pinned), vec![tree.clone()]);
1271
1272        // The gate: not in shell.allow, spelled differently, a runtime pin, another platform.
1273        assert!(resolve(&operator, &["bash"], &empty_path).is_empty());
1274        assert!(resolve(&operator, &["Fixture-prog"], &empty_path).is_empty());
1275        let runtime = [tool(
1276            LockOrigin::Runtime {
1277                session: "ses_x".to_string(),
1278            },
1279            &download,
1280            PROGRAM,
1281        )];
1282        assert!(resolve(&runtime, &[PROGRAM], &empty_path).is_empty());
1283        assert!(resolve_pinned_programs(
1284            &operator,
1285            &allow(&[PROGRAM]),
1286            &empty_path,
1287            Ok(&store),
1288            Some("darwin-aarch64"),
1289        )
1290        .is_empty());
1291        assert!(resolve_pinned_programs(
1292            &operator,
1293            &allow(&[PROGRAM]),
1294            &empty_path,
1295            Ok(&store),
1296            None
1297        )
1298        .is_empty());
1299
1300        // A host program of that name wins.
1301        let host_dir = dir.path().join("host-bin");
1302        std::fs::create_dir(&host_dir).unwrap();
1303        let host = host_dir.join(PROGRAM);
1304        std::fs::write(&host, "#!/bin/sh\necho host-copy-ran\n").unwrap();
1305        std::fs::set_permissions(&host, std::fs::Permissions::from_mode(0o755)).unwrap();
1306        let chosen = resolve(&operator, &[PROGRAM], std::slice::from_ref(&host_dir));
1307        assert_eq!(
1308            chosen[0].source,
1309            PinnedSource::Host {
1310                path: std::fs::canonicalize(&host).unwrap()
1311            }
1312        );
1313        assert!(pinned_bin_dirs(&chosen).is_empty());
1314
1315        // A record naming another sha, and a missing link, are missing.
1316        let record_path = tree.join(RECORD_FILE);
1317        let record_text = std::fs::read_to_string(&record_path).unwrap();
1318        std::fs::write(
1319            &record_path,
1320            record_text.replace(&download.sha256, &"ab".repeat(32)),
1321        )
1322        .unwrap();
1323        assert!(matches!(
1324            &resolve(&operator, &[PROGRAM], &empty_path)[0].source,
1325            PinnedSource::Missing { reason } if reason.contains(&format!("names sha256 {}", "ab".repeat(32)))
1326        ));
1327        std::fs::write(&record_path, record_text).unwrap();
1328        std::fs::remove_file(tree.join("bin").join(PROGRAM)).unwrap();
1329        assert!(matches!(
1330            &resolve(&operator, &[PROGRAM], &empty_path)[0].source,
1331            PinnedSource::Missing { reason } if reason.contains("has no runnable bin/fixture-prog")
1332        ));
1333        let store_err = resolve_pinned_programs(
1334            &operator,
1335            &allow(&[PROGRAM]),
1336            &empty_path,
1337            Err("HOME is not set"),
1338            Some("linux-x86_64"),
1339        );
1340        assert!(matches!(
1341            &store_err[0].source,
1342            PinnedSource::Missing { reason } if reason.contains("HOME is not set")
1343        ));
1344    }
1345}