1#![forbid(unsafe_code)]
8
9mod directive;
10mod error;
11mod staging;
12#[cfg(unix)]
13mod transaction;
14#[cfg(not(unix))]
15#[path = "transaction_non_unix.rs"]
16mod transaction;
17mod unix;
18mod validation;
19
20use std::path::{Path, PathBuf};
21use std::time::Duration;
22
23pub use error::{Result, UpdateError};
24pub use staging::StagedArtifact;
25pub use transaction::{ConfirmationOutcome, InstallOutcome};
26#[cfg(unix)]
27pub use unix::reexec;
28pub use unix::restart_command;
29pub use validation::ValidatedArtifact;
30
31#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub enum ArtifactTransportPolicy {
34 HttpsOnly,
36 HttpsOrLoopbackHttp,
38}
39
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub enum BackupStrategy {
43 HardLinkOrCopy,
45 Copy,
47}
48
49#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct UpdateDirective {
52 version: String,
53 artifact_url: String,
54 sha256: String,
55}
56
57impl UpdateDirective {
58 pub fn new(
60 version: impl Into<String>,
61 artifact_url: impl Into<String>,
62 sha256: impl Into<String>,
63 ) -> Result<Self> {
64 let version = version.into();
65 let artifact_url = artifact_url.into();
66 let sha256 = sha256.into();
67 if version.trim().is_empty() {
68 return Err(UpdateError::InvalidDirective("version must not be empty"));
69 }
70 if artifact_url.trim().is_empty() {
71 return Err(UpdateError::InvalidDirective(
72 "artifact URL must not be empty",
73 ));
74 }
75 if sha256.len() != 64 || !sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) {
76 return Err(UpdateError::InvalidDigest(sha256));
77 }
78 Ok(Self {
79 version,
80 artifact_url,
81 sha256: sha256.to_ascii_lowercase(),
82 })
83 }
84
85 pub fn version(&self) -> &str {
87 &self.version
88 }
89
90 pub fn artifact_url(&self) -> &str {
92 &self.artifact_url
93 }
94
95 pub fn sha256(&self) -> &str {
97 &self.sha256
98 }
99}
100
101#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct UpdateLayout {
104 executable: PathBuf,
105 state_file: PathBuf,
106}
107
108impl UpdateLayout {
109 pub fn new(executable: impl Into<PathBuf>, state_file: impl Into<PathBuf>) -> Self {
111 Self {
112 executable: executable.into(),
113 state_file: state_file.into(),
114 }
115 }
116
117 pub fn executable(&self) -> &Path {
119 &self.executable
120 }
121
122 pub fn state_file(&self) -> &Path {
124 &self.state_file
125 }
126}
127
128#[derive(Clone, Debug, Eq, PartialEq)]
130pub struct UpdatePolicy {
131 transport: ArtifactTransportPolicy,
132 max_artifact_bytes: u64,
133 validation_timeout: Duration,
134 max_unconfirmed_restarts: u32,
135 backup_strategy: BackupStrategy,
136}
137
138impl Default for UpdatePolicy {
139 fn default() -> Self {
140 Self {
141 transport: ArtifactTransportPolicy::HttpsOnly,
142 max_artifact_bytes: 128 * 1024 * 1024,
143 validation_timeout: Duration::from_secs(10),
144 max_unconfirmed_restarts: 3,
145 backup_strategy: BackupStrategy::HardLinkOrCopy,
146 }
147 }
148}
149
150impl UpdatePolicy {
151 pub fn with_transport(mut self, transport: ArtifactTransportPolicy) -> Self {
153 self.transport = transport;
154 self
155 }
156
157 pub fn with_max_artifact_bytes(mut self, bytes: u64) -> Result<Self> {
159 if bytes == 0 {
160 return Err(UpdateError::InvalidPolicy(
161 "maximum artifact size must be greater than zero",
162 ));
163 }
164 self.max_artifact_bytes = bytes;
165 Ok(self)
166 }
167
168 pub fn with_validation_timeout(mut self, timeout: Duration) -> Result<Self> {
170 if timeout.is_zero() {
171 return Err(UpdateError::InvalidPolicy(
172 "validation timeout must be greater than zero",
173 ));
174 }
175 self.validation_timeout = timeout;
176 Ok(self)
177 }
178
179 pub fn with_max_unconfirmed_restarts(mut self, attempts: u32) -> Result<Self> {
181 if attempts == 0 {
182 return Err(UpdateError::InvalidPolicy(
183 "restart limit must be greater than zero",
184 ));
185 }
186 self.max_unconfirmed_restarts = attempts;
187 Ok(self)
188 }
189
190 pub fn with_backup_strategy(mut self, strategy: BackupStrategy) -> Self {
191 self.backup_strategy = strategy;
192 self
193 }
194
195 pub fn transport(&self) -> ArtifactTransportPolicy {
196 self.transport
197 }
198
199 pub fn max_artifact_bytes(&self) -> u64 {
200 self.max_artifact_bytes
201 }
202
203 pub fn validation_timeout(&self) -> Duration {
204 self.validation_timeout
205 }
206
207 pub fn max_unconfirmed_restarts(&self) -> u32 {
208 self.max_unconfirmed_restarts
209 }
210
211 pub fn backup_strategy(&self) -> BackupStrategy {
212 self.backup_strategy
213 }
214}
215
216#[derive(Clone, Debug)]
218pub struct Updater {
219 layout: UpdateLayout,
220 policy: UpdatePolicy,
221 layout_resolution_error: Option<LayoutBindingError>,
222 #[cfg(all(test, unix))]
223 test_failpoint: std::sync::Arc<std::sync::atomic::AtomicU8>,
224}
225
226#[derive(Clone, Debug)]
228pub enum MigrationOutcome {
229 Migrated { updater: Updater },
231 MigratedIndeterminate {
238 updater: Updater,
239 diagnostic: String,
240 },
241}
242
243impl MigrationOutcome {
244 pub fn into_updater(self) -> Updater {
246 match self {
247 Self::Migrated { updater } | Self::MigratedIndeterminate { updater, .. } => updater,
248 }
249 }
250
251 pub fn updater(&self) -> &Updater {
253 match self {
254 Self::Migrated { updater } | Self::MigratedIndeterminate { updater, .. } => updater,
255 }
256 }
257}
258
259impl Updater {
260 pub fn new(layout: UpdateLayout, policy: UpdatePolicy) -> Self {
261 let (layout, layout_resolution_error) = bind_layout_to_current_dir(layout);
262 Self {
263 layout,
264 policy,
265 layout_resolution_error,
266 #[cfg(all(test, unix))]
267 test_failpoint: std::sync::Arc::new(std::sync::atomic::AtomicU8::new(0)),
268 }
269 }
270
271 pub fn layout(&self) -> &UpdateLayout {
272 &self.layout
273 }
274
275 pub fn policy(&self) -> &UpdatePolicy {
276 &self.policy
277 }
278
279 pub(crate) fn ensure_layout_bound(&self) -> Result<()> {
280 match &self.layout_resolution_error {
281 Some(error) => Err(UpdateError::io(
282 &error.path,
283 std::io::Error::new(error.kind, error.message.clone()),
284 )),
285 None => Ok(()),
286 }
287 }
288}
289
290#[derive(Clone, Debug)]
291struct LayoutBindingError {
292 path: PathBuf,
293 kind: std::io::ErrorKind,
294 message: String,
295}
296
297fn bind_layout_to_current_dir(layout: UpdateLayout) -> (UpdateLayout, Option<LayoutBindingError>) {
298 let base = if layout.executable.is_absolute() && layout.state_file.is_absolute() {
299 None
300 } else {
301 match std::env::current_dir() {
302 Ok(base) => Some(base),
303 Err(error) => {
304 return (
305 layout,
306 Some(LayoutBindingError {
307 path: PathBuf::from("."),
308 kind: error.kind(),
309 message: error.to_string(),
310 }),
311 );
312 }
313 }
314 };
315 let executable = if layout.executable.is_absolute() {
316 layout.executable
317 } else {
318 base.as_ref().unwrap().join(layout.executable)
319 };
320 let state_file = if layout.state_file.is_absolute() {
321 layout.state_file
322 } else {
323 base.as_ref().unwrap().join(layout.state_file)
324 };
325 #[cfg(unix)]
326 let state_file = match bind_state_identity(&state_file) {
327 Ok(state_file) => state_file,
328 Err(error) => {
329 let diagnostic = LayoutBindingError {
330 path: state_file.clone(),
331 kind: error.kind(),
332 message: error.to_string(),
333 };
334 return (
335 UpdateLayout {
336 executable,
337 state_file,
338 },
339 Some(diagnostic),
340 );
341 }
342 };
343 (
344 UpdateLayout {
345 executable,
346 state_file,
347 },
348 None,
349 )
350}
351
352#[cfg(unix)]
353pub(crate) fn bind_state_identity(path: &Path) -> std::io::Result<PathBuf> {
354 let mut component_path = PathBuf::new();
355 for component in path.components() {
356 component_path.push(component);
357 match std::fs::symlink_metadata(&component_path) {
358 Ok(metadata) if metadata.file_type().is_symlink() => {
359 return Err(std::io::Error::new(
360 std::io::ErrorKind::InvalidInput,
361 "state path must not contain symlinked components",
362 ));
363 }
364 Ok(_) => {}
365 Err(error) if error.kind() == std::io::ErrorKind::NotFound => break,
366 Err(error) => return Err(error),
367 }
368 }
369 let identity = match std::fs::canonicalize(path) {
370 Ok(path) => Ok(path),
371 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
372 let parent = path.parent().ok_or_else(|| {
373 std::io::Error::new(
374 std::io::ErrorKind::InvalidInput,
375 "state file must have a parent",
376 )
377 })?;
378 let parent = std::fs::canonicalize(parent)?;
379 let name = path.file_name().ok_or_else(|| {
380 std::io::Error::new(
381 std::io::ErrorKind::InvalidInput,
382 "state file must have a name",
383 )
384 })?;
385 Ok(parent.join(name))
386 }
387 Err(error) => Err(error),
388 }?;
389 if identity != path {
390 return Err(std::io::Error::new(
391 std::io::ErrorKind::InvalidInput,
392 "state path must be canonical and contain no symlinked components",
393 ));
394 }
395 Ok(identity)
396}
397
398pub(crate) fn reject_executable_leaf_symlink(path: &Path) -> Result<()> {
399 match std::fs::symlink_metadata(path) {
400 Ok(metadata) if metadata.file_type().is_symlink() => Err(UpdateError::InvalidPolicy(
401 "executable path must not be a symlink",
402 )),
403 Ok(_) => Ok(()),
404 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
405 Err(error) => Err(UpdateError::io(path, error)),
406 }
407}
408
409#[derive(Clone, Debug, Eq, PartialEq)]
411pub enum RecoveryAction {
412 NoPendingUpdate,
413 PendingUpdate {
414 target: String,
415 attempts: u32,
416 max_attempts: u32,
417 },
418 RollbackInstalled {
419 executable: PathBuf,
420 restored_version: String,
421 },
422}