1#![deny(rustdoc::broken_intra_doc_links)]
13#![deny(rustdoc::private_intra_doc_links)]
14#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
15
16pub mod common_args;
17mod detail;
18pub mod metadata;
19pub(crate) mod mockable;
20pub mod result;
21
22use std::fs;
23
24use cargo_metadata::camino::{Utf8Path, Utf8PathBuf};
25use log::{debug, error, info, trace, warn};
26pub use result::Error;
27pub use result::Result;
28use toml_edit::{ImDocument, Item, Table};
29
30use crate::detail::{merge_msrv_dependencies, PACKAGE_SECTION_NAME};
31#[mockall_double::double]
32use crate::mockable::fs as mockable_fs;
33use crate::result::IoErrorContext;
34
35pub const DEFAULT_MANIFEST_BACKUP_SUFFIX: &str = ".msrv-prep.bak";
37
38pub const RUST_VERSION_SPECIFIER: &str = "rust-version";
40
41pub const DEFAULT_MANIFEST_FILE_NAME: &str = "Cargo.toml";
43
44pub const LOCKFILE_EXT: &str = "lock";
46
47pub fn remove_rust_version(manifest: &mut Table) -> bool {
52 trace!("Entering `remove_rust_version`");
53
54 let changed = match manifest.get_mut(PACKAGE_SECTION_NAME) {
55 Some(Item::Table(package)) => {
56 info!(
57 "'package' section found in manifest, removing '{}' field",
58 RUST_VERSION_SPECIFIER
59 );
60
61 package.remove(RUST_VERSION_SPECIFIER).is_some()
62 },
63 _ => false,
64 };
65
66 trace!("Exiting `remove_rust_version` (changed: {})", changed);
67 changed
68}
69
70pub fn maybe_merge_msrv_dependencies(
76 manifest: &mut Table,
77 manifest_path: &Utf8Path,
78 pins_file_name: &str,
79) -> Result<bool> {
80 trace!(
81 "Entering `maybe_merge_msrv_dependencies` (manifest_path: '{}', pins_file_name: '{}')",
82 manifest_path,
83 pins_file_name
84 );
85 let mut changed = false;
86
87 let pins_file_path = manifest_path.parent().map(|par| par.join(pins_file_name));
88
89 if let Some(pins_file_path) = pins_file_path {
90 debug!("Pinned MSRV dependencies file path: {}", pins_file_path);
91
92 if pins_file_path.is_file() {
93 info!(
94 "Pinned MSRV dependencies file found at '{}'; merging with manifest at '{}'",
95 pins_file_path, manifest_path
96 );
97
98 let pins_file_text = fs::read_to_string(&pins_file_path)
99 .with_io_context(|| format!("reading MSRV pins file '{}'", pins_file_path))?;
100 let pins_file = ImDocument::parse(pins_file_text)?;
101
102 changed = merge_msrv_dependencies(manifest, &pins_file);
103 }
104 } else {
105 warn!("Pinned MSRV dependencies file path could not be determined; skipping");
106 }
107
108 trace!("Exiting `maybe_merge_msrv_dependencies` (changed: {})", changed);
109 Ok(changed)
110}
111
112pub fn backup_manifest(manifest_path: &Utf8Path, backup_suffix: &str, force: bool) -> Result<()> {
118 trace!(
119 "Entering `backup_manifest` (manifest_path: '{}', backup_suffix: '{}', force: {})",
120 manifest_path,
121 backup_suffix,
122 force,
123 );
124
125 let lockfile_path = manifest_path.with_extension(LOCKFILE_EXT);
126
127 let manifest_backup_path = get_backup_path(manifest_path, backup_suffix)?;
128 let lockfile_backup_path = get_backup_path(&lockfile_path, backup_suffix)?;
129
130 validate_backup_file(&manifest_backup_path, force)?;
131 if lockfile_path.is_file() {
132 validate_backup_file(&lockfile_backup_path, force)?;
133 }
134
135 backup_file(manifest_path, &manifest_backup_path)?;
136 if lockfile_path.is_file() {
137 backup_file(&lockfile_path, &lockfile_backup_path)?;
138 }
139
140 trace!("Exiting `backup_manifest`");
141 Ok(())
142}
143
144pub fn maybe_restore_manifest(manifest_path: &Utf8Path, backup_suffix: &str) -> Result<()> {
151 trace!(
152 "Entering `maybe_restore_manifest` (manifest_path: '{}', backup_suffix: '{}')",
153 manifest_path,
154 backup_suffix
155 );
156
157 let lockfile_path = manifest_path.with_extension(LOCKFILE_EXT);
158
159 maybe_restore_file(manifest_path, backup_suffix)?;
160
161 if lockfile_path.is_file() {
162 maybe_restore_file(&lockfile_path, backup_suffix)?;
163 }
164
165 trace!("Exiting `maybe_restore_manifest`");
166 Ok(())
167}
168
169fn maybe_restore_file(file_path: &Utf8Path, backup_suffix: &str) -> Result<()> {
170 trace!(
171 "Entering `maybe_restore_file` (file_path: '{}', backup_suffix: '{}')",
172 file_path,
173 backup_suffix
174 );
175
176 let backup_path = get_backup_path(file_path, backup_suffix)?;
177 debug!("Backup path: {}", backup_path);
178
179 if backup_path.is_file() {
180 info!("Backup file found at '{}'; restoring to '{}'", backup_path, file_path);
181
182 mockable_fs::rename(&backup_path, file_path).with_io_context(|| {
183 format!("restoring backup from '{}' to '{}'", backup_path, file_path)
184 })?;
185 }
186
187 trace!("Exiting `maybe_restore_file`");
188 Ok(())
189}
190
191fn get_backup_path(file_path: &Utf8Path, backup_suffix: &str) -> Result<Utf8PathBuf> {
192 file_path
193 .file_name()
194 .map(|name| name.to_string() + backup_suffix)
195 .and_then(|name| file_path.parent().map(|par| par.join(name)))
196 .ok_or_else(|| Error::InvalidPath(file_path.into()))
197}
198
199fn validate_backup_file(backup_path: &Utf8Path, force: bool) -> Result<()> {
200 match (backup_path.is_file(), force) {
201 (true, true) => {
202 info!(
203 "Backup file already exists at '{}'; will be overwritten (forced backup)",
204 backup_path
205 );
206 Ok(())
207 },
208 (true, false) => {
209 error!("Backup file already exists at '{}'; aborting", backup_path);
210
211 Err(Error::BackupFileAlreadyExists(backup_path.into()))
212 },
213 (false, _) => Ok(()),
214 }
215}
216
217fn backup_file(file_path: &Utf8Path, backup_path: &Utf8Path) -> Result<()> {
218 info!("Backing up '{}' to '{}'", file_path, backup_path);
219 mockable_fs::copy(file_path, backup_path)
220 .map(|_| ())
221 .with_io_context(|| format!("backing up '{}' to '{}'", file_path, backup_path))
222}
223
224#[cfg(test)]
225#[cfg_attr(coverage_nightly, coverage(off))]
226mod tests {
227 use super::*;
228
229 mod remove_rust_version {
230 use indoc::indoc;
231 use toml_edit::DocumentMut;
232
233 use super::*;
234
235 #[test_log::test]
236 fn no_rust_version() {
237 let manifest_text = indoc! {r#"
238 [table]
239 hangar = 23
240 "#};
241 let mut manifest = manifest_text.parse::<DocumentMut>().unwrap();
242
243 let changed = remove_rust_version(&mut manifest);
244
245 assert!(!changed);
246 assert_eq!(manifest_text, manifest.to_string());
247 }
248 }
249
250 mod maybe_merge_msrv_dependencies {
251 use assert_matches::assert_matches;
252
253 use super::*;
254
255 #[test_log::test]
256 fn skip_parent_path() {
257 let changed =
258 maybe_merge_msrv_dependencies(&mut Table::new(), "".into(), "msrv-pins.toml");
259
260 assert_matches!(changed, Ok(false));
261 }
262 }
263
264 mod backup_manifest {
265 use super::*;
266
267 mod errors {
268 use std::io;
269
270 use assert_matches::assert_matches;
271
272 use super::*;
273
274 #[test_log::test]
275 fn backup_copy_error() {
276 let project_path: Utf8PathBuf = [
277 env!("CARGO_MANIFEST_DIR"),
278 "resources",
279 "tests",
280 "cargo-msrv-prep",
281 "simple_project",
282 ]
283 .iter()
284 .collect();
285
286 let ctx = mockable_fs::copy_context();
287 ctx.expect().returning(|_, _| {
288 Err(io::Error::new(io::ErrorKind::PermissionDenied, "permission denied"))
289 });
290
291 let result = backup_manifest(
292 &project_path.join("Cargo.toml"),
293 DEFAULT_MANIFEST_BACKUP_SUFFIX,
294 true,
295 );
296 assert_matches!(result, Err(Error::Io { source, .. }) => {
297 assert_eq!(io::ErrorKind::PermissionDenied, source.kind());
298 });
299 }
300 }
301 }
302
303 mod maybe_restore_manifest {
304 use super::*;
305
306 mod errors {
307 use std::io;
308
309 use assert_matches::assert_matches;
310
311 use super::*;
312
313 #[test_log::test]
314 fn backup_rename_error() {
315 let project_path: Utf8PathBuf = [
316 env!("CARGO_MANIFEST_DIR"),
317 "resources",
318 "tests",
319 "cargo-msrv-unprep",
320 "simple_project",
321 ]
322 .iter()
323 .collect();
324
325 let ctx = mockable_fs::rename_context();
326 ctx.expect().returning(|_, _| {
327 Err(io::Error::new(io::ErrorKind::PermissionDenied, "permission denied"))
328 });
329
330 let result = maybe_restore_manifest(
331 &project_path.join("Cargo.toml"),
332 DEFAULT_MANIFEST_BACKUP_SUFFIX,
333 );
334 assert_matches!(result, Err(Error::Io { source, .. }) => {
335 assert_eq!(io::ErrorKind::PermissionDenied, source.kind());
336 });
337 }
338 }
339 }
340}