Skip to main content

eggup_core/
current_exe.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use crate::error::{Error, Result};
5
6/// The exact executable that is currently running, bound once to a canonical target.
7///
8/// Binding resolves symlinks, so an invocation through a link updates the real
9/// image and never overwrites the link object. It also proves that the target is
10/// a regular file beneath a real directory and records an identity for it: on
11/// Unix that is device plus inode, and on **every** platform it is the SHA-256
12/// of the image's bytes.
13///
14/// The content digest is what makes the binding safe on Windows. A file
15/// identity there needs handle-based FFI, which this crate forbids, so a
16/// path-only recheck would silently accept an image swapped in between binding
17/// and commit. Content equality is the strongest identity provable with safe
18/// code on every platform, and a mismatch fails closed.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct CurrentExecutable {
21    target: PathBuf,
22    installation_root: PathBuf,
23    destination_name: PathBuf,
24    digest: [u8; 32],
25    #[cfg(unix)]
26    device: u64,
27    #[cfg(unix)]
28    inode: u64,
29}
30
31impl CurrentExecutable {
32    /// Resolves and binds the running executable from the process environment.
33    ///
34    /// `std::env::current_exe` is only a starting point: on some platforms it
35    /// reports the invocation path, which may traverse symlinks. The result is
36    /// always canonicalized and re-proven before it is used for mutation.
37    ///
38    /// # Errors
39    ///
40    /// Returns [`Error::Io`] when the running executable cannot be reported or
41    /// canonicalized, [`Error::InvalidInput`] when the resolved path is not
42    /// absolute or has no parent directory, and [`Error::DestinationConflict`]
43    /// when the resolved target is not a regular, non-symlinked file beneath a
44    /// real directory — including a hard-linked image, whose exact identity
45    /// cannot be proven.
46    pub fn resolve() -> Result<Self> {
47        let reported = std::env::current_exe()
48            .map_err(|source| Error::io("resolving current executable", source))?;
49        Self::bind(reported)
50    }
51
52    /// Binds an already-known executable path, applying the same proof rules as
53    /// [`resolve`](Self::resolve).
54    ///
55    /// This is the seam a child-process fixture uses to address a specific
56    /// image; production callers normally use [`resolve`](Self::resolve).
57    ///
58    /// # Errors
59    ///
60    /// Returns [`Error::InvalidInput`] when `executable` is not an absolute
61    /// path, when the canonicalized target is empty, or when the target has
62    /// no parent directory or no file name; [`Error::Io`] when the target or
63    /// its directory cannot be read or canonicalized; and
64    /// [`Error::DestinationConflict`] when the target is not a regular,
65    /// non-symlinked file beneath a real directory — including a hard-linked
66    /// image, whose exact identity cannot be proven.
67    pub fn bind(executable: impl Into<PathBuf>) -> Result<Self> {
68        let reported = executable.into();
69        if !reported.is_absolute() {
70            return Err(Error::invalid("current executable path must be absolute"));
71        }
72        // Canonicalization is what makes an invocation symlink safe: the link
73        // object is never the destination, only its real target is.
74        let target = fs::canonicalize(&reported)
75            .map_err(|source| Error::io("canonicalizing current executable", source))?;
76        if target.as_os_str().is_empty() {
77            return Err(Error::invalid(
78                "current executable resolved to an empty path",
79            ));
80        }
81        let metadata = fs::symlink_metadata(&target)
82            .map_err(|source| Error::io("reading current executable metadata", source))?;
83        if metadata.file_type().is_symlink() || !metadata.is_file() {
84            return Err(Error::DestinationConflict {
85                destination: target,
86            });
87        }
88        #[cfg(unix)]
89        {
90            use std::os::unix::fs::MetadataExt;
91            // A hard-linked image has other names whose contents would silently
92            // change with it, so exact identity is not provable and the update
93            // is refused.
94            if metadata.nlink() != 1 {
95                return Err(Error::DestinationConflict {
96                    destination: target,
97                });
98            }
99        }
100        let installation_root = target
101            .parent()
102            .ok_or_else(|| Error::invalid("current executable has no parent directory"))?
103            .to_path_buf();
104        let root_metadata = fs::symlink_metadata(&installation_root)
105            .map_err(|source| Error::io("reading executable directory metadata", source))?;
106        if root_metadata.file_type().is_symlink() || !root_metadata.is_dir() {
107            return Err(Error::DestinationConflict {
108                destination: target,
109            });
110        }
111        let destination_name = PathBuf::from(
112            target
113                .file_name()
114                .ok_or_else(|| Error::invalid("current executable has no file name"))?,
115        );
116        #[cfg(unix)]
117        let (device, inode) = {
118            use std::os::unix::fs::MetadataExt;
119            (metadata.dev(), metadata.ino())
120        };
121        // Read once here so a later swap is detectable without any unsafe code.
122        let digest = crate::integrity::hash_file(&target)?;
123        Ok(Self {
124            target,
125            installation_root,
126            destination_name,
127            digest,
128            #[cfg(unix)]
129            device,
130            #[cfg(unix)]
131            inode,
132        })
133    }
134
135    /// Returns the canonical live path that will be replaced.
136    pub fn target(&self) -> &Path {
137        &self.target
138    }
139
140    /// Returns the executable's own directory, which is the installation root
141    /// used by [`crate::InstallPlan::for_current_executable`].
142    ///
143    /// The whole self-update transaction stays inside this directory, so no
144    /// write authority above it is ever required.
145    pub fn installation_root(&self) -> &Path {
146        &self.installation_root
147    }
148
149    /// Returns the file name used as the single destination member, relative to
150    /// [`installation_root`](Self::installation_root).
151    pub fn destination_name(&self) -> &Path {
152        &self.destination_name
153    }
154
155    /// Returns the absolute destination path of the running image.
156    pub fn destination(&self) -> PathBuf {
157        self.installation_root.join(&self.destination_name)
158    }
159
160    /// Re-proves that this exact object is still the bound current executable.
161    ///
162    /// Called immediately before live mutation under the mutation lock. A
163    /// replaced, swapped, or re-linked image fails closed instead of being
164    /// overwritten.
165    pub(crate) fn revalidate(&self) -> Result<()> {
166        let metadata = match fs::symlink_metadata(&self.target) {
167            Ok(metadata) => metadata,
168            Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
169                return Err(Error::DestinationConflict {
170                    destination: self.target.clone(),
171                })
172            }
173            Err(source) => {
174                return Err(Error::io(
175                    "reading current executable before mutation",
176                    source,
177                ))
178            }
179        };
180        if metadata.file_type().is_symlink() || !metadata.is_file() {
181            return Err(Error::DestinationConflict {
182                destination: self.target.clone(),
183            });
184        }
185        #[cfg(unix)]
186        {
187            use std::os::unix::fs::MetadataExt;
188            if metadata.nlink() != 1
189                || metadata.dev() != self.device
190                || metadata.ino() != self.inode
191            {
192                return Err(Error::DestinationConflict {
193                    destination: self.target.clone(),
194                });
195            }
196        }
197        // Checked on every platform. Without this, the Windows build has *no*
198        // identity proof at all — device and inode do not exist there — and
199        // would commit over an image swapped in after binding.
200        match crate::integrity::hash_file(&self.target) {
201            Ok(digest) if digest == self.digest => Ok(()),
202            Ok(_) => Err(Error::DestinationConflict {
203                destination: self.target.clone(),
204            }),
205            // Already a typed crate error; propagate it unchanged so the
206            // diagnostic names the real filesystem failure.
207            Err(error) => Err(error),
208        }
209    }
210}