Repository: mozilla/cargo-vet (rust)

Issue:
Notes generated by mach cargo vet are unexpectedly deleted by running mach vendor rust
Notes fields generated with `mach cargo vet` are being unexpectedly deleted during `mach vendor rust`. I discussed with @bholley , who suggested that I file an issue here.

I'm able to reproduce the behavior with the `Cargo.toml` line from [this patch](https://phabricator.services.mozilla.com/D159637):

```
+ cubeb-pulse = { git = "https://github.com/mozilla/cubeb-pulse-rs", rev="cf48897be5cbe147d051ebbbe1eaf5fd8fb6bbc9", optional = true, features=["pulse-dlopen"] }
```

Here's some command output:

```
# This first command doesn't seem to have an effect, but leaving it in just in case the output is helpful at all
$ ./mach cargo vet
ERROR 
  × 'cargo metadata' exited unsuccessfully
  ╰─▶ `cargo metadata` exited with an error:     Updating git repository `https://github.com/mozilla/cubeb-pulse-rs`
      error: the lock file /home/ashley/git/mozilla-unified/Cargo.lock needs to be updated but --locked was passed to prevent this
      If you want to try to generate the lock file without accessing the network, remove the --locked flag and use --offline instead.

$ ./mach vendor rust
# ...
    Removing cubeb-pulse v0.4.0 (https://github.com/mozilla/cubeb-pulse-rs?rev=79096eb59b0bfcedb660716a67c0de34a25ea29e#79096eb5)                                                                                   
      Adding cubeb-pulse v0.4.1 (https://github.com/mozilla/cubeb-pulse-rs?rev=cf48897be5cbe147d051ebbbe1eaf5fd8fb6bbc9#cf48897b)                                                                                   
    Removing pulse v0.3.0 (https://github.com/mozilla/cubeb-pulse-rs?rev=79096eb59b0bfcedb660716a67c0de34a25ea29e#79096eb5)                                                                                         
      Adding pulse v0.3.0 (https://github.com/mozilla/cubeb-pulse-rs?rev=cf48897be5cbe147d051ebbbe1eaf5fd8fb6bbc9#cf48897b)                                                                                         
    Removing pulse-ffi v0.1.0 (https://github.com/mozilla/cubeb-pulse-rs?rev=79096eb59b0bfcedb660716a67c0de34a25ea29e#79096eb5)                                                                                     
      Adding pulse-ffi v0.1.0 (https://github.com/mozilla/cubeb-pulse-rs?rev=cf48897be5cbe147d051ebbbe1eaf5fd8fb6bbc9#cf48897b)
# ...

# Notes are removed by this point!
$ ./mach cargo vet
Vetting Succeeded (132 fully audited, 70 partially audited, 252 exempted)

# snip from diff output
diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml
index 0ce3dbc2e61e2..efaf9ba795b75 100644
--- a/supply-chain/audits.toml
+++ b/supply-chain/audits.toml
@@ -266,9 +266,6 @@ delta = "0.10.1 -> 0.10.2"
 who = "Paul Adenot <paul@paul.cx>"
 criteria = "safe-to-deploy"
 delta = "0.10.2 -> 0.10.3"
-note = """
-Mozilla-developed package.
-"""
 
 [[audits.cubeb-core]]
 who = "Matthew Gregan <kinetik@flim.org>"
@@ -287,9 +284,6 @@ delta = "0.10.1 -> 0.10.2"
 who = "Paul Adenot <paul@paul.cx>"
 criteria = "safe-to-deploy"
 delta = "0.10.2 -> 0.10.3"
-note = """
-Mozilla-developed package.
-"""
```

Pull request description:
When running --locked, check formatting of store files
This will detect issues like audits being out of order and unrecognized fields in CI, and should help avoid simple executions of `cargo vet` causing formatting changes locally.

The implementation isn't particularly efficient for this, in that it immediately attempts to re-serialize the file after parsing it, and then compares the results (ignoring trailing newlines). As this checks formatting, it is a string-based check, and not structural.

The `similar` library, which we already used for our tests, is used to allow including diffs in the output to make any issues more clear. This also required some changes to the way we read/write files from disk to keep information around.

As a side-effect of these changes, we no longer emit an extra unnecessary newline at the end of store files. This won't be detected as an error by the checker, but will be a change when run locally.

Fixes #341

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 579b696..2603fad 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1166,9 +1166,9 @@ dependencies = [
 
 [[package]]
 name = "similar"
-version = "2.1.0"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3"
+checksum = "62ac7f900db32bf3fd12e0117dd3dc4da74bc52ebaac97f39668446d89694803"
 
 [[package]]
 name = "slab"
diff --git a/Cargo.toml b/Cargo.toml
index 92d72c6..07cb256 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -34,6 +34,7 @@ nom = "7.1.1"
 reqwest = { version = "0.11.10", default-features = false, features = ["rustls-tls"] }
 serde = "1.0.136"
 serde_json = "1.0.82"
+similar = "2.2.0"
 tar = { version = "0.4.26", default-features = false }
 tempfile = "3.3.0"
 textwrap = { version = "0.15", default-features = false }
@@ -58,4 +59,3 @@ features = [
 
 [dev-dependencies]
 insta = "1.16.0"
-similar = "2.1.0"
diff --git a/src/errors.rs b/src/errors.rs
index b6e91a6..778eafb 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -1,12 +1,77 @@
-use std::{ffi::OsString, fmt::Display, path::PathBuf, string::FromUtf8Error, sync::Arc};
+use std::{
+    ffi::OsString,
+    fmt::{Debug, Display},
+    path::PathBuf,
+    string::FromUtf8Error,
+    sync::Arc,
+};
 
 use cargo_metadata::Version;
-use miette::{Diagnostic, NamedSource, SourceOffset, SourceSpan};
+use miette::{Diagnostic, MietteSpanContents, SourceCode, SourceOffset, SourceSpan};
 use thiserror::Error;
 
 use crate::format::{CriteriaName, ForeignCriteriaName, ImportName, PackageName};
 
-pub type SourceFile = Arc<NamedSource>;
+#[derive(Eq, PartialEq)]
+struct SourceFileInner {
+    name: String,
+    source: String,
+}
+
+#[derive(Clone, Eq, PartialEq)]
+pub struct SourceFile {
+    inner: Arc<SourceFileInner>,
+}
+
+impl SourceFile {
+    pub fn new_empty(name: &str) -> Self {
+        Self::new(name, String::new())
+    }
+    pub fn new(name: &str, source: String) -> Self {
+        SourceFile {
+            inner: Arc::new(SourceFileInner {
+                name: name.to_owned(),
+                source,
+            }),
+        }
+    }
+    pub fn name(&self) -> &str {
+        &self.inner.name
+    }
+    pub fn source(&self) -> &str {
+        &self.inner.source
+    }
+}
+
+impl SourceCode for SourceFile {
+    fn read_span<'a>(
+        &'a self,
+        span: &SourceSpan,
+        context_lines_before: usize,
+        context_lines_after: usize,
+    ) -> Result<Box<dyn miette::SpanContents<'a> + 'a>, miette::MietteError> {
+        let contents = self
+            .source()
+            .read_span(span, context_lines_before, context_lines_after)?;
+        Ok(Box::new(MietteSpanContents::new_named(
+            self.name().to_owned(),
+            contents.data(),
+            *contents.span(),
+            contents.line(),
+            contents.column(),
+            contents.line_count(),
+        )))
+    }
+}
+
+impl Debug for SourceFile {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("SourceFile")
+            .field("name", &self.name())
+            .field("source", &self.source())
+            .finish()
+    }
+}
 
 ///////////////////////////////////////////////////////////
 // AuditAsErrors
@@ -212,16 +277,12 @@ pub struct CriteriaChangeErrors {
     pub errors: Vec<CriteriaChangeError>,
 }
 #[derive(Debug, Error, Diagnostic)]
-// FIXME: it would be rad if this was a diff!
-#[error(
-    "{import_name}'s '{criteria_name}' criteria changed from\n\n{old_desc}\n\nto\n\n{new_desc}\n"
-)]
+#[error("{import_name}'s '{criteria_name}' criteria changed:\n\n{unified_diff}")]
 #[diagnostic(help("Run `cargo vet regenerate imports` to accept this new definition"))]
 pub struct CriteriaChangeError {
     pub import_name: ImportName,
     pub criteria_name: ForeignCriteriaName,
-    pub old_desc: String,
-    pub new_desc: String,
+    pub unified_diff: String,
 }
 
 ////////////////////////////////////////////////////////////
@@ -241,6 +302,9 @@ pub enum StoreValidateError {
     #[diagnostic(transparent)]
     #[error(transparent)]
     InvalidCriteria(InvalidCriteriaError),
+    #[diagnostic(transparent)]
+    #[error(transparent)]
+    BadFormat(BadFormatError),
     #[error("imports.lock is out-of-date with respect to configuration")]
     #[diagnostic(help("run `cargo vet` without --locked to update imports"))]
     ImportsLockOutdated,
@@ -258,6 +322,13 @@ pub struct InvalidCriteriaError {
     pub valid_names: Arc<Vec<String>>,
 }
 
+#[derive(Debug, Error, Diagnostic)]
+#[error("A file in the store is not correctly formatted:\n\n{unified_diff}")]
+#[diagnostic(help("run `cargo vet` without --locked to reformat files in the store"))]
+pub struct BadFormatError {
+    pub unified_diff: String,
+}
+
 //////////////////////////////////////////////////////////
 // CacheErrors
 /////////////////////////////////////////////////////////
@@ -691,28 +762,6 @@ pub enum LoadJsonError {
     ),
 }
 
-#[derive(Debug, Error, Diagnostic)]
-#[non_exhaustive]
-pub enum StoreJsonError {
-    #[error(transparent)]
-    JsonSerialize(#[from] serde_json::Error),
-    #[error("couldn't store json")]
-    IoError(
-        #[from]
-        #[source]
-        std::io::Error,
-    ),
-}
+pub type StoreJsonError = serde_json::Error;
 
-#[derive(Debug, Error, Diagnostic)]
-#[non_exhaustive]
-pub enum StoreTomlError {
-    #[error(transparent)]
-    TomlSerialize(#[from] toml_edit::ser::Error),
-    #[error("couldn't store toml")]
-    IoError(
-        #[from]
-        #[source]
-        std::io::Error,
-    ),
-}
+pub type StoreTomlError = toml_edit::ser::Error;
diff --git a/src/main.rs b/src/main.rs
index 24a4ea5..d337f7f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -20,7 +20,7 @@ use format::{CriteriaName, CriteriaStr, PackageName, PolicyEntry};
 use futures_util::future::{join_all, try_join_all};
 use indicatif::ProgressDrawTarget;
 use lazy_static::lazy_static;
-use miette::{miette, Context, Diagnostic, IntoDiagnostic, NamedSource, SourceOffset};
+use miette::{miette, Context, Diagnostic, IntoDiagnostic, SourceOffset};
 use network::Network;
 use out::{progress_bar, IncProgressOnDrop};
 use reqwest::Url;
@@ -30,7 +30,7 @@ use thiserror::Error;
 use tracing::{error, info, trace, warn};
 
 use crate::cli::*;
-use crate::errors::{CommandError, DownloadError, RegenerateExemptionsError};
+use crate::errors::{CommandError, DownloadError, RegenerateExemptionsError, SourceFile};
 use crate::format::{
     AuditEntry, AuditKind, AuditsFile, ConfigFile, CriteriaEntry, DependencyCriteria,
     ExemptedDependency, FetchCommand, ImportsFile, MetaConfig, MetaConfigInstance, PackageStr,
@@ -1610,7 +1610,7 @@ fn cmd_aggregate(
             let url_string = url.to_string();
             let audit_bytes = network.download(url).await?;
             let audit_string = String::from_utf8(audit_bytes).map_err(LoadTomlError::from)?;
-            let audit_source = Arc::new(NamedSource::new(url_string.clone(), audit_string.clone()));
+            let audit_source = SourceFile::new(&url_string, audit_string.clone());
             let audit_file: AuditsFile = toml::de::from_str(&audit_string)
                 .map_err(|error| {
                     let (line, col) = error.line_col().unwrap_or((0, 0));
diff --git a/src/storage.rs b/src/storage.rs
index 767ad59..cfee557 100644
--- a/src/storage.rs
+++ b/src/storage.rs
@@ -11,15 +11,16 @@ use std::{
 use cargo_metadata::Version;
 use flate2::read::GzDecoder;
 use futures_util::future::try_join_all;
-use miette::{NamedSource, SourceOffset};
+use miette::SourceOffset;
 use reqwest::Url;
 use serde::{Deserialize, Serialize};
+use similar::{udiff::unified_diff, Algorithm};
 use tar::Archive;
 use tracing::{error, info, log::warn, trace};
 
 use crate::{
     errors::{
-        CacheAcquireError, CacheCommitError, CommandError, CriteriaChangeError,
+        BadFormatError, CacheAcquireError, CacheCommitError, CommandError, CriteriaChangeError,
         CriteriaChangeErrors, DiffError, DownloadError, FetchAndDiffError, FetchAuditError,
         FetchError, FlockError, InvalidCriteriaError, JsonParseError, LoadJsonError, LoadTomlError,
         SourceFile, StoreAcquireError, StoreCommitError, StoreCreateError, StoreJsonError,
@@ -165,9 +166,9 @@ impl Store {
                 audits: SortedMap::new(),
             },
             live_imports: None,
-            config_src: Arc::new(NamedSource::new(CONFIG_TOML, "")),
-            audits_src: Arc::new(NamedSource::new(AUDITS_TOML, "")),
-            imports_src: Arc::new(NamedSource::new(IMPORTS_LOCK, "")),
+            config_src: SourceFile::new_empty(CONFIG_TOML),
+            audits_src: SourceFile::new_empty(AUDITS_TOML),
+            imports_src: SourceFile::new_empty(IMPORTS_LOCK),
         })
     }
 
@@ -226,7 +227,7 @@ impl Store {
         };
 
         // Check that the store isn't corrupt
-        store.validate()?;
+        store.validate(cfg.cli.locked)?;
 
         Ok(store)
     }
@@ -240,9 +241,9 @@ impl Store {
             imports,
             audits,
             live_imports: None,
-            config_src: Arc::new(NamedSource::new(CONFIG_TOML, "")),
-            audits_src: Arc::new(NamedSource::new(AUDITS_TOML, "")),
-            imports_src: Arc::new(NamedSource::new(IMPORTS_LOCK, "")),
+            config_src: SourceFile::new_empty(CONFIG_TOML),
+            audits_src: SourceFile::new_empty(AUDITS_TOML),
+            imports_src: SourceFile::new_empty(IMPORTS_LOCK),
         }
     }
 
@@ -264,9 +265,9 @@ impl Store {
             imports,
             audits,
             live_imports: Some(live_imports),
-            config_src: Arc::new(NamedSource::new(CONFIG_TOML, "")),
-            audits_src: Arc::new(NamedSource::new(AUDITS_TOML, "")),
-            imports_src: Arc::new(NamedSource::new(IMPORTS_LOCK, "")),
+            config_src: SourceFile::new_empty(CONFIG_TOML),
+            audits_src: SourceFile::new_empty(AUDITS_TOML),
+            imports_src: SourceFile::new_empty(IMPORTS_LOCK),
         })
     }
 
@@ -275,6 +276,7 @@ impl Store {
         config: &str,
         audits: &str,
         imports: &str,
+        check_file_formatting: bool,
     ) -> Result<Self, StoreAcquireError> {
         let (config_src, config): (_, ConfigFile) = load_toml(CONFIG_TOML, config.as_bytes())?;
         let (audits_src, audits): (_, AuditsFile) = load_toml(AUDITS_TOML, audits.as_bytes())?;
@@ -291,7 +293,7 @@ impl Store {
             imports_src,
         };
 
-        store.validate()?;
+        store.validate(check_file_formatting)?;
 
         Ok(store)
     }
@@ -340,12 +342,12 @@ impl Store {
         // TODO: make this truly transactional?
         // (With a dir rename? Does that work with the lock? Fine because it's already closed?)
         if let Some(lock) = self.lock {
-            let audits = lock.write_audits()?;
-            let config = lock.write_config()?;
-            let imports = lock.write_imports()?;
-            store_audits(audits, self.audits)?;
-            store_config(config, self.config)?;
-            store_imports(imports, self.imports)?;
+            let mut audits = lock.write_audits()?;
+            let mut config = lock.write_config()?;
+            let mut imports = lock.write_imports()?;
+            audits.write_all(store_audits(self.audits)?.as_bytes())?;
+            config.write_all(store_config(self.config)?.as_bytes())?;
+            imports.write_all(store_imports(self.imports)?.as_bytes())?;
         }
         Ok(())
     }
@@ -354,17 +356,19 @@ impl Store {
     /// Doesn't take `self` by value so that it can continue to be used.
     #[cfg(test)]
     pub fn mock_commit(&self) -> SortedMap<String, String> {
-        let mut audits = Vec::new();
-        let mut config = Vec::new();
-        let mut imports = Vec::new();
-        store_audits(&mut audits, self.audits.clone()).unwrap();
-        store_config(&mut config, self.config.clone()).unwrap();
-        store_imports(&mut imports, self.imports.clone()).unwrap();
-
         [
-            (AUDITS_TOML.to_owned(), String::from_utf8(audits).unwrap()),
-            (CONFIG_TOML.to_owned(), String::from_utf8(config).unwrap()),
-            (IMPORTS_LOCK.to_owned(), String::from_utf8(imports).unwrap()),
+            (
+                AUDITS_TOML.to_owned(),
+                store_audits(self.audits.clone()).unwrap(),
+            ),
+            (
+                CONFIG_TOML.to_owned(),
+                store_config(self.config.clone()).unwrap(),
+            ),
+            (
+                IMPORTS_LOCK.to_owned(),
+                store_imports(self.imports.clone()).unwrap(),
+            ),
         ]
         .into_iter()
         .collect()
@@ -372,7 +376,7 @@ impl Store {
 
     /// Validate the store's integrity
     #[allow(clippy::for_kv_map)]
-    pub fn validate(&self) -> Result<(), StoreValidateErrors> {
+    pub fn validate(&self, check_file_formatting: bool) -> Result<(), StoreValidateErrors> {
         // ERRORS: ideally these are all gathered diagnostics, want to report as many errors
         // at once as possible!
 
@@ -512,6 +516,46 @@ impl Store {
             }
         }
 
+        // If requested, verify that files in the store are correctly formatted
+        // and have no unrecognized fields. We don't want to be reformatting
+        // them or dropping unused fields while in CI, as those changes will be
+        // ignored.
+        let mut bad_format_errors = Vec::new();
+        if check_file_formatting {
+            for (name, old, new) in [
+                (
+                    CONFIG_TOML,
+                    self.config_src.source(),
+                    store_config(self.config.clone())
+                        .unwrap_or_else(|_| self.config_src.source().to_owned()),
+                ),
+                (
+                    AUDITS_TOML,
+                    self.audits_src.source(),
+                    store_audits(self.audits.clone())
+                        .unwrap_or_else(|_| self.audits_src.source().to_owned()),
+                ),
+                (
+                    IMPORTS_LOCK,
+                    self.imports_src.source(),
+                    store_imports(self.imports.clone())
+                        .unwrap_or_else(|_| self.imports_src.source().to_owned()),
+                ),
+            ] {
+                if old.trim_end() != new.trim_end() {
+                    bad_format_errors.push(BadFormatError {
+                        unified_diff: unified_diff(
+                            Algorithm::Myers,
+                            old,
+                            &new,
+                            3,
+                            Some((&format!("old/{name}"), &format!("new/{name}"))),
+                        ),
+                    });
+                }
+            }
+        }
+
         // If we're locked, and therefore not fetching new live imports,
         // validate that our imports.lock is in sync with config.toml.
         let imports_lock_outdated = self
@@ -522,6 +566,11 @@ impl Store {
             .into_iter()
             .map(StoreValidateError::InvalidCriteria)
             .chain(imports_lock_outdated)
+            .chain(
+                bad_format_errors
+                    .into_iter()
+                    .map(StoreValidateError::BadFormat),
+            )
             .collect::<Vec<_>>();
         if !errors.is_empty() {
             return Err(StoreValidateErrors { errors });
@@ -690,8 +739,13 @@ fn process_imported_audits(
                             changed_criteria.push(CriteriaChangeError {
                                 import_name: import_name.clone(),
                                 criteria_name: criteria_name.to_owned(),
-                                old_desc: old_desc.clone(),
-                                new_desc: new_desc.clone(),
+                                unified_diff: unified_diff(
+                                    Algorithm::Myers,
+                                    old_desc,
+                                    new_desc,
+                                    5,
+                                    None,
+                                ),
                             });
                         }
                     }
@@ -768,7 +822,7 @@ async fn fetch_imported_audit(
     })?;
     let audit_bytes = network.download(parsed_url).await?;
     let audit_string = String::from_utf8(audit_bytes).map_err(LoadTomlError::from)?;
-    let audit_source = Arc::new(NamedSource::new(name, audit_string.clone()));
+    let audit_source = SourceFile::new(name, audit_string.clone());
     let mut audit_file: AuditsFile = toml::de::from_str(&audit_string)
         .map_err(|error| {
             let (line, col) = error.line_col().unwrap_or((0, 0));
@@ -886,10 +940,8 @@ impl Drop for Cache {
         if let Some(diff_cache_path) = &self.diff_cache_path {
             // Write back the diff_cache
             if let Err(err) = || -> Result<(), CacheCommitError> {
-                store_diff_cache(
-                    File::create(diff_cache_path)?,
-                    mem::take(&mut state.diff_cache),
-                )?;
+                let diff_cache = store_diff_cache(mem::take(&mut state.diff_cache))?;
+                fs::write(diff_cache_path, diff_cache)?;
                 Ok(())
             }() {
                 error!("error writing back changes to diff-cache: {:?}", err);
@@ -898,10 +950,8 @@ impl Drop for Cache {
         if let Some(command_history_path) = &self.command_history_path {
             // Write back the command_history
             if let Err(err) = || -> Result<(), CacheCommitError> {
-                store_command_history(
-                    File::create(command_history_path)?,
-                    mem::take(&mut state.command_history),
-                )?;
+                let command_history = store_command_history(mem::take(&mut state.command_history))?;
+                fs::write(command_history_path, command_history)?;
                 Ok(())
             }() {
                 error!("error writing back changes to diff-cache: {:?}", err);
@@ -1595,16 +1645,15 @@ where
     let mut reader = BufReader::new(reader);
     let mut string = String::new();
     reader.read_to_string(&mut string)?;
-    let result = toml::de::from_str(&string);
-    // We defer the NamedSource creation into each branch because the error path wants to
-    // have access to the input string, but NamedSource consumes and erases it.
+    let source_code = SourceFile::new(file_name, string);
+    let result = toml::de::from_str(source_code.source());
     match result {
-        Ok(toml) => Ok((Arc::new(NamedSource::new(file_name, string)), toml)),
+        Ok(toml) => Ok((source_code, toml)),
         Err(error) => {
             let (line, col) = error.line_col().unwrap_or((0, 0));
-            let span = SourceOffset::from_location(&string, line + 1, col);
+            let span = SourceOffset::from_location(source_code.source(), line + 1, col);
             Err(TomlParseError {
-                source_code: Arc::new(NamedSource::new(file_name, string)),
+                source_code,
                 span,
                 error,
             }
@@ -1612,14 +1661,12 @@ where
         }
     }
 }
-fn store_toml<T>(mut writer: impl Write, heading: &str, val: T) -> Result<(), StoreTomlError>
+fn store_toml<T>(heading: &str, val: T) -> Result<String, StoreTomlError>
 where
     T: Serialize,
 {
-    // FIXME: do this in a temp file and swap it into place to avoid corruption?
     let toml_document = to_formatted_toml(val)?;
-    writeln!(writer, "{}{}", heading, toml_document)?;
-    Ok(())
+    Ok(format!("{}{}", heading, toml_document))
 }
 fn load_json<T>(reader: impl Read) -> Result<T, LoadJsonError>
 where
@@ -1631,17 +1678,15 @@ where
     let json = serde_json::from_str(&string).map_err(|error| JsonParseError { error })?;
     Ok(json)
 }
-fn store_json<T>(mut writer: impl Write, val: T) -> Result<(), StoreJsonError>
+fn store_json<T>(val: T) -> Result<String, StoreJsonError>
 where
     T: Serialize,
 {
-    // FIXME: do this in a temp file and swap it into place to avoid corruption?
     let json_string = serde_json::to_string(&val)?;
-    writeln!(writer, "{}", json_string)?;
-    Ok(())
+    Ok(json_string)
 }
 
-fn store_audits(writer: impl Write, mut audits: AuditsFile) -> Result<(), StoreTomlError> {
+fn store_audits(mut audits: AuditsFile) -> Result<String, StoreTomlError> {
     let heading = r###"
 # cargo-vet audits file
 "###;
@@ -1650,10 +1695,9 @@ fn store_audits(writer: impl Write, mut audits: AuditsFile) -> Result<(), StoreT
         .values_mut()
         .for_each(|entries| entries.sort());
 
-    store_toml(writer, heading, audits)?;
-    Ok(())
+    store_toml(heading, audits)
 }
-fn store_config(writer: impl Write, mut config: ConfigFile) -> Result<(), StoreTomlError> {
+fn store_config(mut config: ConfigFile) -> Result<String, StoreTomlError> {
     config
         .exemptions
         .values_mut()
@@ -1663,27 +1707,20 @@ fn store_config(writer: impl Write, mut config: ConfigFile) -> Result<(), StoreT
 # cargo-vet config file
 "###;
 
-    store_toml(writer, heading, config)?;
-    Ok(())
+    store_toml(heading, config)
 }
-fn store_imports(writer: impl Write, imports: ImportsFile) -> Result<(), StoreTomlError> {
+fn store_imports(imports: ImportsFile) -> Result<String, StoreTomlError> {
     let heading = r###"
 # cargo-vet imports lock
 "###;
 
-    store_toml(writer, heading, imports)?;
-    Ok(())
+    store_toml(heading, imports)
 }
-fn store_diff_cache(writer: impl Write, diff_cache: DiffCache) -> Result<(), StoreTomlError> {
+fn store_diff_cache(diff_cache: DiffCache) -> Result<String, StoreTomlError> {
     let heading = "";
 
-    store_toml(writer, heading, diff_cache)?;
-    Ok(())
+    store_toml(heading, diff_cache)
 }
-fn store_command_history(
-    writer: impl Write,
-    command_history: CommandHistory,
-) -> Result<(), StoreJsonError> {
-    store_json(writer, command_history)?;
-    Ok(())
+fn store_command_history(command_history: CommandHistory) -> Result<String, StoreJsonError> {
+    store_json(command_history)
 }
diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml
index 5499b45..373a32d 100644
--- a/supply-chain/audits.toml
+++ b/supply-chain/audits.toml
@@ -47,3 +47,12 @@ criteria = "safe-to-deploy"
 version = "1.4.0"
 notes = "I have read over the macros, and audited the unsafe code."
 
+[[audits.similar]]
+who = "Nika Layzell <nika@thelayzells.com>"
+criteria = "safe-to-deploy"
+version = "2.2.0"
+notes = """
+Algorithm crate implemented entirely in safe rust. Does no platform-specific
+logic, only implementing diffing and string manipulation algorithms.
+"""
+
diff --git a/supply-chain/config.toml b/supply-chain/config.toml
index 0c48a20..1055e9d 100644
--- a/supply-chain/config.toml
+++ b/supply-chain/config.toml
@@ -161,10 +161,6 @@ criteria = "safe-to-deploy"
 version = "0.1.19"
 criteria = "safe-to-deploy"
 
-[[exemptions.hex]]
-version = "0.4.3"
-criteria = "safe-to-deploy"
-
 [[exemptions.http]]
 version = "0.2.8"
 criteria = "safe-to-deploy"
@@ -405,10 +401,6 @@ criteria = "safe-to-deploy"
 version = "1.4.0"
 criteria = "safe-to-deploy"
 
-[[exemptions.similar]]
-version = "2.1.0"
-criteria = "safe-to-run"
-
 [[exemptions.slab]]
 version = "0.4.6"
 criteria = "safe-to-deploy"
diff --git a/supply-chain/imports.lock b/supply-chain/imports.lock
index e18750f..7fdc730 100644
--- a/supply-chain/imports.lock
+++ b/supply-chain/imports.lock
@@ -163,6 +163,11 @@ criteria = "safe-to-deploy"
 version = "0.12.3"
 notes = "This version is used in rust's libstd, so effectively we're already trusting it"
 
+[[audits.firefox.audits.hex]]
+who = "Simon Friedberger <simon@mozilla.com>"
+criteria = "safe-to-deploy"
+version = "0.4.3"
+
 [[audits.firefox.audits.hyper]]
 who = "Mike Hommey <mh+mozilla@glandium.org>"
 criteria = "safe-to-run"


Test patch:
diff --git a/src/tests/import.rs b/src/tests/import.rs
index 81a3df4..71564fb 100644
--- a/src/tests/import.rs
+++ b/src/tests/import.rs
@@ -793,6 +793,72 @@ fn peer_audits_import_exclusion() {
     insta::assert_snapshot!(output);
 }
 
+#[test]
+fn existing_peer_updated_description() {
+    // (Pass) If we've previously imported from a peer, and a criteria
+    // description changed, we get an error with details.
+
+    let _enter = TEST_RUNTIME.enter();
+    let mock = MockMetadata::simple();
+
+    let metadata = mock.metadata();
+    let (mut config, audits, mut imports) = builtin_files_full_audited(&metadata);
+
+    let old_foreign_audits = AuditsFile {
+        criteria: [(
+            "example".to_string(),
+            criteria(
+                "Example criteria description\n\
+                First line\n\
+                Second line\n\
+                Third line\n",
+            ),
+        )]
+        .into_iter()
+        .collect(),
+        audits: SortedMap::new(),
+    };
+
+    let new_foreign_audits = AuditsFile {
+        criteria: [(
+            "example".to_string(),
+            criteria(
+                "Example criteria description\n\
+                First new line\n\
+                Third line\n\
+                Fourth line\n",
+            ),
+        )]
+        .into_iter()
+        .collect(),
+        audits: SortedMap::new(),
+    };
+
+    imports
+        .audits
+        .insert(FOREIGN.to_owned(), old_foreign_audits);
+
+    config.imports.insert(
+        FOREIGN.to_owned(),
+        crate::format::RemoteImport {
+            url: FOREIGN_URL.to_owned(),
+            ..Default::default()
+        },
+    );
+
+    let error = match Store::mock_online(
+        config,
+        audits,
+        imports,
+        vec![(FOREIGN.to_owned(), new_foreign_audits)],
+        false,
+    ) {
+        Ok(_) => panic!("expected store creation to fail due to updated criteria"),
+        Err(err) => miette::Report::from(err),
+    };
+    insta::assert_snapshot!("existing_peer_updated_description", format!("{:?}", error));
+}
+
 // Other tests worth adding:
 //
 // - used edges with dependency-criteria should cause criteria to be imported
diff --git a/src/tests/snapshots/cargo_vet__tests__import__existing_peer_updated_description.snap b/src/tests/snapshots/cargo_vet__tests__import__existing_peer_updated_description.snap
new file mode 100644
index 0000000..1e743e6
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__import__existing_peer_updated_description.snap
@@ -0,0 +1,20 @@
+---
+source: src/tests/import.rs
+expression: "format!(\"{:?}\", error)"
+---
+
+  × Some of your imported audits changed their criteria descriptions
+
+Error: 
+  × peer-company's 'example' criteria changed:
+  │ 
+  │ @@ -1,4 +1,4 @@
+  │  Example criteria description
+  │ -First line
+  │ -Second line
+  │ +First new line
+  │  Third line
+  │ +Fourth line
+  │ 
+  help: Run `cargo vet regenerate imports` to accept this new definition
+
diff --git a/src/tests/snapshots/cargo_vet__tests__import__peer_audits_exemption_minimize.snap b/src/tests/snapshots/cargo_vet__tests__import__peer_audits_exemption_minimize.snap
index b570027..336b131 100644
--- a/src/tests/snapshots/cargo_vet__tests__import__peer_audits_exemption_minimize.snap
+++ b/src/tests/snapshots/cargo_vet__tests__import__peer_audits_exemption_minimize.snap
@@ -21,7 +21,6 @@ config.toml:
  [[exemptions.transitive-third-party1]]
  version = "10.0.0"
  criteria = "safe-to-deploy"
- 
 
 imports.lock:
  
@@ -31,6 +30,5 @@ imports.lock:
 +[[audits.peer-company.audits.third-party2]]
 +criteria = "safe-to-deploy"
 +version = "10.0.0"
- 
 
 
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__invalid_formatting.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__invalid_formatting.snap
new file mode 100644
index 0000000..edfa6c6
--- /dev/null
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__invalid_formatting.snap
@@ -0,0 +1,58 @@
+---
+source: src/tests/store_parsing.rs
+expression: acquire_errors
+---
+
+  × Your cargo-vet store (supply-chain) has consistency errors
+
+Error: 
+  × A file in the store is not correctly formatted:
+  │ 
+  │ --- old/config.toml
+  │ +++ new/config.toml
+  │ @@ -8,14 +8,14 @@
+  │  [imports.peer2]
+  │  url = "https://peer1.com"
+  │ 
+  │ -[[exemptions.zzz]]
+  │ +[[exemptions.aaa]]
+  │  version = "1.0.0"
+  │  criteria = "safe-to-deploy"
+  │ 
+  │  [[exemptions.bbb]]
+  │ +version = "1.0.0"
+  │  criteria = "safe-to-deploy"
+  │ -version = "1.0.0"
+  │ 
+  │ -[[exemptions.aaa]]
+  │ +[[exemptions.zzz]]
+  │  version = "1.0.0"
+  │  criteria = "safe-to-deploy"
+  │ 
+  help: run `cargo vet` without --locked to reformat files in the store
+Error: 
+  × A file in the store is not correctly formatted:
+  │ 
+  │ --- old/audits.toml
+  │ +++ new/audits.toml
+  │ @@ -4,14 +4,12 @@
+  │  [criteria.good]
+  │  description = "great"
+  │  implies = "safe-to-deploy"
+  │ -unrecognized-field = "hey there!"
+  │ 
+  │  [[audits.serde]]
+  │  criteria = ["safe-to-deploy", "good"]
+  │ -version = "2.0.0"
+  │ -note = "invalid field"
+  │ +version = "1.0.0"
+  │ +notes = "valid field"
+  │ 
+  │  [[audits.serde]]
+  │  criteria = ["safe-to-deploy", "good"]
+  │ -version = "1.0.0"
+  │ -notes = "valid field"
+  │ +version = "2.0.0"
+  │ 
+  help: run `cargo vet` without --locked to reformat files in the store
+
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_audits.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_audits.snap
index 9f3eb7a..7000975 100644
--- a/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_audits.snap
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_audits.snap
@@ -7,100 +7,101 @@ expression: acquire_errors
 
 Error: 
   × 'bad-imply' is not a valid criteria name
-   ╭─[audits.toml:3:1]
- 3 │ description = "great"
- 4 │ implies = ["safe-to-deploy", "bad-imply"]
+   ╭─[audits.toml:5:1]
+ 5 │ description = "great"
+ 6 │ implies = ["safe-to-deploy", "bad-imply"]
    ·                              ───────────
- 5 │ 
+ 7 │ 
    ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
   × 'bad' is not a valid criteria name
-   ╭─[audits.toml:7:1]
- 7 │ version = "1.0.0"
- 8 │ criteria = "bad"
-   ·            ─────
- 9 │ dependency-criteria = { toml = "bad-dep", serde_derive = ["bad1", "good", "bad2"] }
-   ╰────
+    ╭─[audits.toml:8:1]
+  8 │ [[audits.serde]]
+  9 │ criteria = "bad"
+    ·            ─────
+ 10 │ version = "1.0.0"
+    ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
   × 'bad1' is not a valid criteria name
-    ╭─[audits.toml:8:1]
-  8 │ criteria = "bad"
-  9 │ dependency-criteria = { toml = "bad-dep", serde_derive = ["bad1", "good", "bad2"] }
-    ·                                                           ──────
- 10 │ 
+    ╭─[audits.toml:10:1]
+ 10 │ version = "1.0.0"
+ 11 │ dependency-criteria = { serde_derive = ["bad1", "good", "bad2"], toml = "bad-dep" }
+    ·                                         ──────
+ 12 │ 
     ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
   × 'bad2' is not a valid criteria name
-    ╭─[audits.toml:8:1]
-  8 │ criteria = "bad"
-  9 │ dependency-criteria = { toml = "bad-dep", serde_derive = ["bad1", "good", "bad2"] }
-    ·                                                                           ──────
- 10 │ 
+    ╭─[audits.toml:10:1]
+ 10 │ version = "1.0.0"
+ 11 │ dependency-criteria = { serde_derive = ["bad1", "good", "bad2"], toml = "bad-dep" }
+    ·                                                         ──────
+ 12 │ 
     ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
   × 'bad-dep' is not a valid criteria name
-    ╭─[audits.toml:8:1]
-  8 │ criteria = "bad"
-  9 │ dependency-criteria = { toml = "bad-dep", serde_derive = ["bad1", "good", "bad2"] }
-    ·                                ─────────
- 10 │ 
+    ╭─[audits.toml:10:1]
+ 10 │ version = "1.0.0"
+ 11 │ dependency-criteria = { serde_derive = ["bad1", "good", "bad2"], toml = "bad-dep" }
+    ·                                                                         ─────────
+ 12 │ 
     ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
-  × 'dang' is not a valid criteria name
-    ╭─[audits.toml:12:1]
- 12 │ delta = "1.0.0 -> 1.1.0"
- 13 │ criteria = ["safe-to-run", "dang"]
-    ·                            ──────
- 14 │ dependency-criteria = {}
+  × 'safe-to-jog' is not a valid criteria name
+    ╭─[audits.toml:13:1]
+ 13 │ [[audits.serde]]
+ 14 │ criteria = "safe-to-jog"
+    ·            ─────────────
+ 15 │ version = "2.0.0"
     ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
-  × 'oops' is not a valid criteria name
-    ╭─[audits.toml:17:1]
- 17 │ delta = "1.0.0 -> 1.1.0"
- 18 │ criteria = "oops"
-    ·            ──────
- 19 │ dependency-criteria = { "nope" = "nah" }
+  × 'unsafe-to-destroy' is not a valid criteria name
+    ╭─[audits.toml:15:1]
+ 15 │ version = "2.0.0"
+ 16 │ dependency-criteria = { toml = "unsafe-to-destroy" }
+    ·                                ───────────────────
+ 17 │ 
     ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
-  × 'nah' is not a valid criteria name
+  × 'dang' is not a valid criteria name
     ╭─[audits.toml:18:1]
- 18 │ criteria = "oops"
- 19 │ dependency-criteria = { "nope" = "nah" }
-    ·                                  ─────
- 20 │ 
+ 18 │ [[audits.serde]]
+ 19 │ criteria = ["safe-to-run", "dang"]
+    ·                            ──────
+ 20 │ delta = "1.0.0 -> 1.1.0"
     ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
-  × 'safe-to-jog' is not a valid criteria name
+  × 'oops' is not a valid criteria name
     ╭─[audits.toml:22:1]
- 22 │ version = "2.0.0"
- 23 │ criteria = "safe-to-jog"
-    ·            ─────────────
- 24 │ dependency-criteria = { toml = ["unsafe-to-destroy"] }
+ 22 │ [[audits.serde]]
+ 23 │ criteria = "oops"
+    ·            ──────
+ 24 │ delta = "1.0.0 -> 1.1.0"
     ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
-  × 'unsafe-to-destroy' is not a valid criteria name
-    ╭─[audits.toml:23:1]
- 23 │ criteria = "safe-to-jog"
- 24 │ dependency-criteria = { toml = ["unsafe-to-destroy"] }
-    ·                                ─────────────────────
- 25 │ 
+  × 'nah' is not a valid criteria name
+    ╭─[audits.toml:24:1]
+ 24 │ delta = "1.0.0 -> 1.1.0"
+ 25 │ dependency-criteria = { nope = "nah" }
+    ·                                ─────
+ 26 │ 
     ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
   × 'no-good-bad-bad' is not a valid criteria name
     ╭─[audits.toml:27:1]
- 27 │ violation = "5.0.0 "
+ 27 │ [[audits.serde]]
  28 │ criteria = "no-good-bad-bad"
     ·            ─────────────────
+ 29 │ violation = "^5.0.0"
     ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_config.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_config.snap
index 96d018f..fb3d6d6 100644
--- a/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_config.snap
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__many_bad_config.snap
@@ -7,65 +7,65 @@ expression: acquire_errors
 
 Error: 
   × 'oops' is not a valid criteria name
-    ╭─[config.toml:16:1]
- 16 │ version = "1.0.0"
- 17 │ criteria = "oops"
+    ╭─[config.toml:18:1]
+ 18 │ version = "1.0.0"
+ 19 │ criteria = "oops"
     ·            ──────
- 18 │ dependency-criteria = { clap_derive = "nah", oops = ["no", "safe-to-run"] }
+ 20 │ dependency-criteria = { clap_derive = "nah", oops = ["no", "safe-to-run"] }
     ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
   × 'nah' is not a valid criteria name
-    ╭─[config.toml:17:1]
- 17 │ criteria = "oops"
- 18 │ dependency-criteria = { clap_derive = "nah", oops = ["no", "safe-to-run"] }
+    ╭─[config.toml:19:1]
+ 19 │ criteria = "oops"
+ 20 │ dependency-criteria = { clap_derive = "nah", oops = ["no", "safe-to-run"] }
     ·                                       ─────
- 19 │ 
+ 21 │ 
     ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
   × 'no' is not a valid criteria name
-    ╭─[config.toml:17:1]
- 17 │ criteria = "oops"
- 18 │ dependency-criteria = { clap_derive = "nah", oops = ["no", "safe-to-run"] }
+    ╭─[config.toml:19:1]
+ 19 │ criteria = "oops"
+ 20 │ dependency-criteria = { clap_derive = "nah", oops = ["no", "safe-to-run"] }
     ·                                                      ────
- 19 │ 
+ 21 │ 
     ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
   × 'bad' is not a valid criteria name
-   ╭─[config.toml:2:1]
- 2 │ [policy.serde]
- 3 │ criteria = "bad"
-   ·            ─────
- 4 │ dev-criteria = "nope"
-   ╰────
+    ╭─[config.toml:12:1]
+ 12 │ [policy.serde]
+ 13 │ criteria = "bad"
+    ·            ─────
+ 14 │ dev-criteria = "nope"
+    ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
   × 'nope' is not a valid criteria name
-   ╭─[config.toml:3:1]
- 3 │ criteria = "bad"
- 4 │ dev-criteria = "nope"
-   ·                ──────
- 5 │ dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
-   ╰────
+    ╭─[config.toml:13:1]
+ 13 │ criteria = "bad"
+ 14 │ dev-criteria = "nope"
+    ·                ──────
+ 15 │ dependency-criteria = { clap = ["safe-to-run", "unsafe-for-all", "good"], serde_derive = "nada" }
+    ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
   × 'unsafe-for-all' is not a valid criteria name
-   ╭─[config.toml:4:1]
- 4 │ dev-criteria = "nope"
- 5 │ dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
-   ·                                                                       ────────────────
- 6 │ 
-   ╰────
+    ╭─[config.toml:14:1]
+ 14 │ dev-criteria = "nope"
+ 15 │ dependency-criteria = { clap = ["safe-to-run", "unsafe-for-all", "good"], serde_derive = "nada" }
+    ·                                                ────────────────
+ 16 │ 
+    ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 Error: 
   × 'nada' is not a valid criteria name
-   ╭─[config.toml:4:1]
- 4 │ dev-criteria = "nope"
- 5 │ dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
-   ·                                        ──────
- 6 │ 
-   ╰────
+    ╭─[config.toml:14:1]
+ 14 │ dev-criteria = "nope"
+ 15 │ dependency-criteria = { clap = ["safe-to-run", "unsafe-for-all", "good"], serde_derive = "nada" }
+    ·                                                                                          ──────
+ 16 │ 
+    ╰────
   help: the possible criteria are ["good", "safe-to-run", "safe-to-deploy"]
 
diff --git a/src/tests/snapshots/cargo_vet__tests__store_parsing__simple_bad_audit.snap b/src/tests/snapshots/cargo_vet__tests__store_parsing__simple_bad_audit.snap
index a3ba998..59299d0 100644
--- a/src/tests/snapshots/cargo_vet__tests__store_parsing__simple_bad_audit.snap
+++ b/src/tests/snapshots/cargo_vet__tests__store_parsing__simple_bad_audit.snap
@@ -7,10 +7,11 @@ expression: acquire_errors
 
 Error: 
   × 'bad' is not a valid criteria name
-   ╭─[audits.toml:3:1]
- 3 │ version = "1.0.0"
- 4 │ criteria = "bad"
+   ╭─[audits.toml:4:1]
+ 4 │ [[audits.serde]]
+ 5 │ criteria = "bad"
    ·            ─────
+ 6 │ version = "1.0.0"
    ╰────
   help: the possible criteria are ["safe-to-run", "safe-to-deploy"]
 
diff --git a/src/tests/store_parsing.rs b/src/tests/store_parsing.rs
index 18a4c34..62791a5 100644
--- a/src/tests/store_parsing.rs
+++ b/src/tests/store_parsing.rs
@@ -1,9 +1,19 @@
-const EMPTY_CONFIG: &str = "\n";
-const EMPTY_AUDITS: &str = "[audits]\n";
-const EMPTY_IMPORTS: &str = "[audits]\n";
+const EMPTY_CONFIG: &str = r##"
+# cargo-vet config file
+"##;
+const EMPTY_AUDITS: &str = r##"
+# cargo-vet audits file
+
+[audits]
+"##;
+const EMPTY_IMPORTS: &str = r##"
+# cargo-vet imports lock
+
+[audits]
+"##;
 
 fn get_valid_store(config: &str, audits: &str, imports: &str) -> String {
-    let res = crate::Store::mock_acquire(config, audits, imports);
+    let res = crate::Store::mock_acquire(config, audits, imports, true);
     match res {
         Ok(_) => String::new(),
         Err(e) => format!("{:?}", miette::Report::new(e)),
@@ -25,9 +35,11 @@ fn test_all_min() {
 #[test]
 fn test_simple_bad_audit() {
     let audits = r##"
+# cargo-vet audits file
+
 [[audits.serde]]
-version = "1.0.0"
 criteria = "bad"
+version = "1.0.0"
 "##;
 
     let acquire_errors = get_valid_store(EMPTY_CONFIG, audits, EMPTY_IMPORTS);
@@ -37,33 +49,34 @@ criteria = "bad"
 #[test]
 fn test_many_bad_audits() {
     let audits = r##"
+# cargo-vet audits file
+
 [criteria.good]
 description = "great"
 implies = ["safe-to-deploy", "bad-imply"]
 
 [[audits.serde]]
-version = "1.0.0"
 criteria = "bad"
-dependency-criteria = { toml = "bad-dep", serde_derive = ["bad1", "good", "bad2"] }
+version = "1.0.0"
+dependency-criteria = { serde_derive = ["bad1", "good", "bad2"], toml = "bad-dep" }
 
 [[audits.serde]]
-delta = "1.0.0 -> 1.1.0"
-criteria = ["safe-to-run", "dang"]
-dependency-criteria = {}
+criteria = "safe-to-jog"
+version = "2.0.0"
+dependency-criteria = { toml = "unsafe-to-destroy" }
 
 [[audits.serde]]
+criteria = ["safe-to-run", "dang"]
 delta = "1.0.0 -> 1.1.0"
-criteria = "oops"
-dependency-criteria = { "nope" = "nah" }
 
 [[audits.serde]]
-version = "2.0.0"
-criteria = "safe-to-jog"
-dependency-criteria = { toml = ["unsafe-to-destroy"] }
+criteria = "oops"
+delta = "1.0.0 -> 1.1.0"
+dependency-criteria = { nope = "nah" }
 
 [[audits.serde]]
-violation = "5.0.0 "
 criteria = "no-good-bad-bad"
+violation = "^5.0.0"
 "##;
 
     let acquire_errors = get_valid_store(EMPTY_CONFIG, audits, EMPTY_IMPORTS);
@@ -73,18 +86,20 @@ criteria = "no-good-bad-bad"
 #[test]
 fn test_many_bad_config() {
     let config = r##"
-[policy.serde]
-criteria = "bad"
-dev-criteria = "nope"
-dependency-criteria = { serde_derive = "nada", clap = ["safe-to-run", "unsafe-for-all", "good"] }
+# cargo-vet config file
+
+[policy.boring]
+audit-as-crates-io = true
 
 [policy.clap]
 criteria = "safe-to-deploy"
 dev-criteria = "safe-to-run"
 dependency-criteria = { clap_derive = "good" }
 
-[policy.boring]
-audit-as-crates-io = true
+[policy.serde]
+criteria = "bad"
+dev-criteria = "nope"
+dependency-criteria = { clap = ["safe-to-run", "unsafe-for-all", "good"], serde_derive = "nada" }
 
 [[exemptions.clap]]
 version = "1.0.0"
@@ -94,13 +109,14 @@ dependency-criteria = { clap_derive = "nah", oops = ["no", "safe-to-run"] }
 [[exemptions.clap_derive]]
 version = "1.0.0"
 criteria = "safe-to-run"
-
 "##;
 
     let audits = r##"
+# cargo-vet audits file
+
 [criteria.good]
 description = "great"
-implies = ["safe-to-deploy"]
+implies = "safe-to-deploy"
 
 [audits]
 "##;
@@ -112,13 +128,15 @@ implies = ["safe-to-deploy"]
 #[test]
 fn test_outdated_imports_lock_extra_peer() {
     let config = r##"
+# cargo-vet config file
+
 [imports.peer1]
 url = "https://peer1.com"
-criteria-map = []
-
 "##;
 
     let imports = r##"
+# cargo-vet imports lock
+
 [[audits.peer1.audits.third-party1]]
 criteria = "safe-to-deploy"
 version = "10.0.0"
@@ -126,7 +144,6 @@ version = "10.0.0"
 [[audits.peer2.audits.third-party2]]
 criteria = "safe-to-deploy"
 version = "10.0.0"
-
 "##;
 
     let acquire_errors = get_valid_store(config, EMPTY_AUDITS, imports);
@@ -136,21 +153,21 @@ version = "10.0.0"
 #[test]
 fn test_outdated_imports_lock_missing_peer() {
     let config = r##"
+# cargo-vet config file
+
 [imports.peer1]
 url = "https://peer1.com"
-criteria-map = []
 
 [imports.peer2]
 url = "https://peer2.com"
-criteria-map = []
-
 "##;
 
     let imports = r##"
+# cargo-vet imports lock
+
 [[audits.peer1.audits.third-party1]]
 criteria = "safe-to-deploy"
 version = "10.0.0"
-
 "##;
 
     let acquire_errors = get_valid_store(config, EMPTY_AUDITS, imports);
@@ -160,14 +177,16 @@ version = "10.0.0"
 #[test]
 fn test_outdated_imports_lock_excluded_crate() {
     let config = r##"
+# cargo-vet config file
+
 [imports.peer1]
 url = "https://peer1.com"
 exclude = ["third-party1"]
-criteria-map = []
-
 "##;
 
     let imports = r##"
+# cargo-vet imports lock
+
 [[audits.peer1.audits.third-party1]]
 criteria = "safe-to-deploy"
 version = "10.0.0"
@@ -175,8 +194,6 @@ version = "10.0.0"
 [[audits.peer1.audits.third-party2]]
 criteria = "safe-to-deploy"
 version = "10.0.0"
-
-
 "##;
 
     let acquire_errors = get_valid_store(config, EMPTY_AUDITS, imports);
@@ -186,18 +203,19 @@ version = "10.0.0"
 #[test]
 fn test_outdated_imports_lock_ok() {
     let config = r##"
+# cargo-vet config file
+
 [imports.peer1]
 url = "https://peer1.com"
 exclude = ["third-party2"]
-criteria-map = []
 
 [imports.peer2]
 url = "https://peer1.com"
-criteria-map = []
-
 "##;
 
     let imports = r##"
+# cargo-vet imports lock
+
 [[audits.peer1.audits.third-party1]]
 criteria = "safe-to-deploy"
 version = "10.0.0"
@@ -205,9 +223,68 @@ version = "10.0.0"
 [[audits.peer2.audits.third-party2]]
 criteria = "safe-to-deploy"
 version = "10.0.0"
-
 "##;
 
     let acquire_errors = get_valid_store(config, EMPTY_AUDITS, imports);
     insta::assert_snapshot!(acquire_errors);
 }
+
+#[test]
+fn test_invalid_formatting() {
+    let config = r##"
+# cargo-vet config file
+
+[imports.peer1]
+url = "https://peer1.com"
+exclude = ["zzz", "aaa"]
+
+[imports.peer2]
+url = "https://peer1.com"
+
+[[exemptions.zzz]]
+version = "1.0.0"
+criteria = "safe-to-deploy"
+
+[[exemptions.bbb]]
+criteria = "safe-to-deploy"
+version = "1.0.0"
+
+[[exemptions.aaa]]
+version = "1.0.0"
+criteria = "safe-to-deploy"
+"##;
+
+    let audits = r##"
+# cargo-vet audits file
+
+[criteria.good]
+description = "great"
+implies = "safe-to-deploy"
+unrecognized-field = "hey there!"
+
+[[audits.serde]]
+criteria = ["safe-to-deploy", "good"]
+version = "2.0.0"
+note = "invalid field"
+
+[[audits.serde]]
+criteria = ["safe-to-deploy", "good"]
+version = "1.0.0"
+notes = "valid field"
+"##;
+
+    let imports = r##"
+# cargo-vet imports lock
+
+[[audits.peer1.audits.third-party1]]
+criteria = "safe-to-deploy"
+version = "10.0.0"
+
+[[audits.peer2.audits.third-party2]]
+criteria = "safe-to-deploy"
+version = "10.0.0"
+"##;
+
+    let acquire_errors = get_valid_store(config, audits, imports);
+    insta::assert_snapshot!(acquire_errors);
+}
diff --git a/tests/diff-cache.toml b/tests/diff-cache.toml
index 84492af..4a51564 100644
--- a/tests/diff-cache.toml
+++ b/tests/diff-cache.toml
@@ -1757,4 +1757,3 @@ raw = """
  27 files changed, 2704 insertions(+)
 """
 count = 2704
-
diff --git a/tests/test-project/supply-chain/audits.toml b/tests/test-project/supply-chain/audits.toml
index cd39618..be9d15a 100644
--- a/tests/test-project/supply-chain/audits.toml
+++ b/tests/test-project/supply-chain/audits.toml
@@ -84,4 +84,3 @@ notes = "test for partial delta criteria"
 criteria = "safe-to-deploy"
 delta = "0.2.0 -> 0.3.7"
 notes = "test for delta to unaudited"
-
diff --git a/tests/test-project/supply-chain/config.toml b/tests/test-project/supply-chain/config.toml
index 8a922c9..62351c6 100644
--- a/tests/test-project/supply-chain/config.toml
+++ b/tests/test-project/supply-chain/config.toml
@@ -401,4 +401,3 @@ criteria = "safe-to-deploy"
 [[exemptions.winreg]]
 version = "0.10.1"
 criteria = "safe-to-deploy"
-
diff --git a/tests/test-project/supply-chain/imports.lock b/tests/test-project/supply-chain/imports.lock
index 428c8ad..fa91a4b 100644
--- a/tests/test-project/supply-chain/imports.lock
+++ b/tests/test-project/supply-chain/imports.lock
@@ -2,4 +2,3 @@
 # cargo-vet imports lock
 
 [audits]
-