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}