use std::{
    borrow::Cow,
    convert::TryInto,
    env,
    fs::{self, File},
    io::{self, Read, Write},
    path::{Path, PathBuf},
};

use fd_lock::RwLock;
use tempfile::NamedTempFile;
use walkdir::WalkDir;

use crate::SPECIAL_NAMES;
use crate::{pgp, InternalError};
use crate::{Error, Result, Tag};

const PATH_PREFIX_LEN: usize = 2;

const TRACE: bool = false;

/// The data type returned by the merge callback.
///
/// See, for instance, [`CertD::insert`].
pub enum MergeResult<'a> {
    /// Keep the on-disk version.
    Keep,

    /// Use the specified version.
    ///
    /// This is usually a merged version of the on-disk version and the
    /// new version of the certificate.
    DataRef(&'a [u8]),

    /// Use the specified version.
    ///
    /// This is usually a merged version of the on-disk version and the
    /// new version of the certificate.
    Data(Vec<u8>),
}

impl<'a> From<&'a [u8]> for MergeResult<'a> {
    fn from(data: &'a [u8]) -> Self {
        MergeResult::DataRef(data)
    }
}

impl From<Vec<u8>> for MergeResult<'_> {
    fn from(data: Vec<u8>) -> Self {
        MergeResult::Data(data)
    }
}

// Used by `CertD::tag`.
//
// One tag for each directory.
struct CertDTag([Tag; 256]);

impl CertDTag {
    fn null() -> Self {
        Self([Tag(676149182_1608123167); 256])
    }

    fn compress(&self) -> Tag {
        // This is pretty naive.  A hash would be better.  So would a
        // few more bits.
        let mut composite: u64 = 0;
        for (i, tag) in self.0.iter().enumerate() {
            let mut tag = tag.0;

            tag = tag.rotate_right(i as u32);

            composite ^= tag
        }

        Tag(composite)
    }
}

/// A certificate store.
///
/// This is a handle to an on-disk certificate store that can be used
/// to lookup and insert certificates.
///
/// A certificate store contains certificates.  Its main role is to
/// hold certificates indexed by their fingerprint.  (Note:
/// certificates are not indexed by their subkey fingerprints.)  But,
/// it can also store certificates under [special names].  Currently,
/// the specification defines one special name, `trust-root`, which
/// holds the user's local trust root.  Non-standard special names are
/// possible.  These MUST start with an underscore, which SHOULD be
/// immediately followed by the vendor's name, e.g.,
/// `_sequoia_some_special.pgp`.
///
///   [special names]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names
#[derive(Debug)]
pub struct CertD {
    base: PathBuf,
}

impl CertD {
    /// Opens the default certificate store.
    ///
    /// If not explicitly requested otherwise, an application SHOULD
    /// use the [default store].  To use a store with a different
    /// location, use [`CertD::with_base_dir`].
    ///
    /// [default store]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-default-stores-location
    pub fn new() -> Result<CertD> {
        CertD::with_base_dir(Self::user_configured_store_path()?)
    }

    /// Returns the location of the user-configured store.
    ///
    /// If set, this is the value of the environment variable
    /// `PGP_CERT_D`.  Otherwise, it is the default store's path as
    /// returned by [`CertD::default_store_path`].
    pub fn user_configured_store_path() -> Result<PathBuf> {
        if let Some(path) = env::var_os("PGP_CERT_D") {
            Ok(PathBuf::from(path))
        } else {
            CertD::default_store_path()
        }
    }

    /// Returns the location of the default store.
    ///
    /// [The location of the default store] is platform specific.
    /// This returns an error on unsupported platforms.
    ///
    /// [The location of the default store]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#platform-specifics
    pub fn default_store_path() -> Result<PathBuf> {
        Ok(dirs::data_dir()
           .ok_or(Error::UnsupportedPlatform(
               "Default store's path".into()))?
           .join("pgp.cert.d"))
    }

    /// Opens a store with an explicit location.
    ///
    /// Note: Most applications should use the [default store], which
    /// is shared.  The default store can be opened using
    /// [`CertD::new`].
    ///
    ///   [default store]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-default-stores-location
    pub fn with_base_dir<P: AsRef<Path>>(base: P) -> Result<CertD> {
        Ok(CertD {
            base: base.as_ref().into(),
        })
    }

    /// Get the this Certd's base path.
    pub fn base_dir(&self) -> &Path {
        &self.base
    }

    /// Computes the certificate directory's tag.
    ///
    /// Modulo collisions in the hashing algorithm, this tag will
    /// change whenever a certificate indexed by its fingerprint is
    /// added, updated, or removed.  The tag is designed to not change
    /// when a certificate indexed by a special name, or some other
    /// unrelated data in the certd directory is added, updated, or
    /// removed.
    ///
    /// The tag should be stable in the sense that you can serialize
    /// it to disk, read it in again later, and compare it to the
    /// current value.  However, how the tag is computed may change.
    /// In this case, you may observe a spurious update.
    pub fn tag(&self) -> Tag {
        let revert_to_readdir = Some(4);

        platform! {
            unix => self.tag_probe_unix(revert_to_readdir),
            windows => self.tag_probe_std(revert_to_readdir),
        }
    }

    /// Never call this function directly!
    ///
    /// This function is not part of the semver contract; It is only
    /// exported to facilitate testing and benchmarking.
    ///
    /// This implements a variant of [`CertD::tag`], which uses
    /// `readdir`, and is implemented using functionality from Rust's
    /// standard library.
    #[doc(hidden)]
    pub fn tag_readdir_std(&self) -> Tag {
        tracer!(TRACE, "CertD::tag_readdir_std");

        let mut composite = CertDTag::null();

        let dir = std::fs::read_dir(&self.base);
        if let Ok(dir) = dir {
            'entry: for e in dir {
                let e = if let Ok(e) = e {
                    e
                } else {
                    continue;
                };

                // Calling file_type is normally free.
                //
                //   https://doc.rust-lang.org/std/fs/struct.DirEntry.html#method.file_type
                //
                // As we are only interested in directories, filter out
                // anything as early as possible.
                if let Ok(file_type) = e.file_type() {
                    if file_type.is_dir() {
                        // Check!
                    } else {
                        continue;
                    }
                } else {
                    continue;
                }

                let filename = e.file_name();
                t!("Examining {:?}", filename);
                let filename: &[u8] = platform! {
                    unix => {
                        use std::os::unix::ffi::OsStrExt;
                        filename.as_bytes()
                    },
                    windows => {
                        if let Some(filename) = filename.to_str() {
                            filename.as_bytes()
                        } else {
                            t!("Can't convert to a str.");
                            continue;
                        }
                    }
                };

                if filename.len() != 2 {
                    t!("Wrong length.");
                    continue;
                }

                let mut nibbles: [u8; 2] = [0; 2];

                for i in 0..2usize {
                    let v = filename[i];
                    nibbles[i] = match v {
                        b'0'..=b'9' => v - b'0',
                        b'a'..=b'f' => 10 + v - b'a',
                        _ => {
                            t!("{}: contains non-lower-hex characters.",
                               String::from_utf8_lossy(filename));
                            continue 'entry;
                        }
                    };
                }
                let i = ((nibbles[0] << 4) + nibbles[1]) as usize;

                // On Windows this is free.  On Unix this requires a
                // system call.
                let metadata = if let Ok(metadata) = e.metadata() {
                    metadata
                } else {
                    t!("{:02x}: Can't read meta-data.", i);
                    continue;
                };

                let tag = if let Ok(tag) = Tag::try_from(metadata) {
                    tag
                } else {
                    t!("Can't compute tag.");
                    continue;
                };

                t!("{:02x} => Tag({:x})", i, tag.0);

                composite.0[i] = tag;
            }
        }

        composite.compress()
    }

    /// Never call this function directly!
    ///
    /// This function is not part of the semver contract; It is only
    /// exported to facilitate testing and benchmarking.
    ///
    /// This implements a variant of [`CertD::tag`], which uses
    /// `readdir`, and is specialized for Unix platforms.
    #[cfg(unix)]
    #[doc(hidden)]
    pub fn tag_readdir_unix(&self) -> Tag {
        use crate::unixdir::Dir;

        tracer!(TRACE, "CertD::tag_readdir_unix");

        let mut composite = CertDTag::null();

        let dir = Dir::open(&self.base);

        if let Ok(mut dir) = dir {
            'entry: while let Some(e) = dir.readdir() {
                let file_type = e.file_type();
                if file_type.is_dir() || file_type.is_unknown() {
                    // Plausible.
                } else {
                    continue;
                }

                let filename = e.file_name();
                t!("Examining {}", String::from_utf8_lossy(filename));
                if filename.len() != 2 {
                    t!("Wrong length.");
                    continue;
                }

                let mut nibbles: [u8; 2] = [0; 2];
                for i in 0..2usize {
                    let v = filename[i];
                    nibbles[i] = match v {
                        b'0'..=b'9' => v - b'0',
                        b'a'..=b'f' => 10 + v - b'a',
                        _ => {
                            t!("{}: contains non-lower-hex characters.",
                               String::from_utf8_lossy(filename));
                            continue 'entry;
                        }
                    };
                }
                let i = ((nibbles[0] << 4) + nibbles[1]) as usize;

                let metadata = if let Ok(metadata) = e.metadata() {
                    metadata
                } else {
                    t!("{:02x}: Can't read meta-data.", i);
                    continue;
                };

                // Double check as the type in the directory entry is
                // not definitive (it could be unknown).
                if ! metadata.is_dir() {
                    t!("{:02x}: Not a directory.");
                    continue;
                }

                let tag = Tag::from(metadata);
                t!("{:02x} => Tag({:x})", i, tag.0);

                composite.0[i] = tag;
            }
        }

        composite.compress()
    }

    /// Never call this function directly!
    ///
    /// This function is not part of the semver contract; It is only
    /// exported to facilitate testing and benchmarking.
    ///
    /// This implements a variant of [`CertD::tag`], which `stats` all
    /// of the expected subdirectories using the Rust standard
    /// library.
    #[doc(hidden)]
    pub fn tag_probe_std(&self, revert_to_readir: Option<usize>) -> Tag {
        tracer!(TRACE, "CertD::tag_probe_std");

        const FILENAMES: [&str; 256] = [
            "00", "01", "02", "03", "04", "05", "06", "07",
            "08", "09", "0a", "0b", "0c", "0d", "0e", "0f",
            "10", "11", "12", "13", "14", "15", "16", "17",
            "18", "19", "1a", "1b", "1c", "1d", "1e", "1f",
            "20", "21", "22", "23", "24", "25", "26", "27",
            "28", "29", "2a", "2b", "2c", "2d", "2e", "2f",
            "30", "31", "32", "33", "34", "35", "36", "37",
            "38", "39", "3a", "3b", "3c", "3d", "3e", "3f",
            "40", "41", "42", "43", "44", "45", "46", "47",
            "48", "49", "4a", "4b", "4c", "4d", "4e", "4f",
            "50", "51", "52", "53", "54", "55", "56", "57",
            "58", "59", "5a", "5b", "5c", "5d", "5e", "5f",
            "60", "61", "62", "63", "64", "65", "66", "67",
            "68", "69", "6a", "6b", "6c", "6d", "6e", "6f",
            "70", "71", "72", "73", "74", "75", "76", "77",
            "78", "79", "7a", "7b", "7c", "7d", "7e", "7f",
            "80", "81", "82", "83", "84", "85", "86", "87",
            "88", "89", "8a", "8b", "8c", "8d", "8e", "8f",
            "90", "91", "92", "93", "94", "95", "96", "97",
            "98", "99", "9a", "9b", "9c", "9d", "9e", "9f",
            "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7",
            "a8", "a9", "aa", "ab", "ac", "ad", "ae", "af",
            "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7",
            "b8", "b9", "ba", "bb", "bc", "bd", "be", "bf",
            "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7",
            "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf",
            "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7",
            "d8", "d9", "da", "db", "dc", "dd", "de", "df",
            "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7",
            "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef",
            "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7",
            "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff",
        ];

        let mut misses = 0;
        let mut hits = 0;

        let mut composite = CertDTag::null();

        let base = self.base_dir();

        for (i, filename) in FILENAMES.iter().enumerate() {
            let mut path = base.to_path_buf();
            path.push(filename);

            let metadata = if let Ok(metadata) = std::fs::metadata(path) {
                hits += 1;
                metadata
            } else {
                t!("{:02x}: No such file or directory.", i);

                misses += 1;

                if let Some(revert_to_readir) = revert_to_readir {
                    if revert_to_readir == misses && hits <= 1 {
                        t!("Too many misses; switching to readir implementation.");
                        return self.tag_readdir_std();
                    }
                }

                continue;
            };

            if ! metadata.file_type().is_dir() {
                t!("{:02x}: Not a directory.");
                continue;
            }

            let tag = match Tag::try_from(&metadata) {
                Ok(tag) => tag,
                Err(err) => {
                    t!("{:02x}: Can't compute tag: {}.", i, err);
                    continue;
                }
            };

            t!("{:02x} => Tag({:x})", i, tag.0);

            composite.0[i] = tag;
        }

        composite.compress()
    }

    /// Never call this function directly!
    ///
    /// This function is not part of the semver contract; It is only
    /// exported to facilitate testing and benchmarking.
    ///
    /// This implements a variant of [`CertD::tag`], which `stats` all
    /// of the expected subdirectories, and is specialized for Unix
    /// platforms.
    #[doc(hidden)]
    #[cfg(unix)]
    pub fn tag_probe_unix(&self, revert_to_readir: Option<usize>) -> Tag {
        use crate::unixdir::Dir;

        tracer!(TRACE, "CertD::tag_probe_unix");

        const FILENAMES: [&str; 256] = [
            "00\0", "01\0", "02\0", "03\0", "04\0", "05\0", "06\0", "07\0",
            "08\0", "09\0", "0a\0", "0b\0", "0c\0", "0d\0", "0e\0", "0f\0",
            "10\0", "11\0", "12\0", "13\0", "14\0", "15\0", "16\0", "17\0",
            "18\0", "19\0", "1a\0", "1b\0", "1c\0", "1d\0", "1e\0", "1f\0",
            "20\0", "21\0", "22\0", "23\0", "24\0", "25\0", "26\0", "27\0",
            "28\0", "29\0", "2a\0", "2b\0", "2c\0", "2d\0", "2e\0", "2f\0",
            "30\0", "31\0", "32\0", "33\0", "34\0", "35\0", "36\0", "37\0",
            "38\0", "39\0", "3a\0", "3b\0", "3c\0", "3d\0", "3e\0", "3f\0",
            "40\0", "41\0", "42\0", "43\0", "44\0", "45\0", "46\0", "47\0",
            "48\0", "49\0", "4a\0", "4b\0", "4c\0", "4d\0", "4e\0", "4f\0",
            "50\0", "51\0", "52\0", "53\0", "54\0", "55\0", "56\0", "57\0",
            "58\0", "59\0", "5a\0", "5b\0", "5c\0", "5d\0", "5e\0", "5f\0",
            "60\0", "61\0", "62\0", "63\0", "64\0", "65\0", "66\0", "67\0",
            "68\0", "69\0", "6a\0", "6b\0", "6c\0", "6d\0", "6e\0", "6f\0",
            "70\0", "71\0", "72\0", "73\0", "74\0", "75\0", "76\0", "77\0",
            "78\0", "79\0", "7a\0", "7b\0", "7c\0", "7d\0", "7e\0", "7f\0",
            "80\0", "81\0", "82\0", "83\0", "84\0", "85\0", "86\0", "87\0",
            "88\0", "89\0", "8a\0", "8b\0", "8c\0", "8d\0", "8e\0", "8f\0",
            "90\0", "91\0", "92\0", "93\0", "94\0", "95\0", "96\0", "97\0",
            "98\0", "99\0", "9a\0", "9b\0", "9c\0", "9d\0", "9e\0", "9f\0",
            "a0\0", "a1\0", "a2\0", "a3\0", "a4\0", "a5\0", "a6\0", "a7\0",
            "a8\0", "a9\0", "aa\0", "ab\0", "ac\0", "ad\0", "ae\0", "af\0",
            "b0\0", "b1\0", "b2\0", "b3\0", "b4\0", "b5\0", "b6\0", "b7\0",
            "b8\0", "b9\0", "ba\0", "bb\0", "bc\0", "bd\0", "be\0", "bf\0",
            "c0\0", "c1\0", "c2\0", "c3\0", "c4\0", "c5\0", "c6\0", "c7\0",
            "c8\0", "c9\0", "ca\0", "cb\0", "cc\0", "cd\0", "ce\0", "cf\0",
            "d0\0", "d1\0", "d2\0", "d3\0", "d4\0", "d5\0", "d6\0", "d7\0",
            "d8\0", "d9\0", "da\0", "db\0", "dc\0", "dd\0", "de\0", "df\0",
            "e0\0", "e1\0", "e2\0", "e3\0", "e4\0", "e5\0", "e6\0", "e7\0",
            "e8\0", "e9\0", "ea\0", "eb\0", "ec\0", "ed\0", "ee\0", "ef\0",
            "f0\0", "f1\0", "f2\0", "f3\0", "f4\0", "f5\0", "f6\0", "f7\0",
            "f8\0", "f9\0", "fa\0", "fb\0", "fc\0", "fd\0", "fe\0", "ff\0",
        ];

        let mut misses = 0;
        let mut hits = 0;

        let mut composite = CertDTag::null();

        let base = self.base_dir();
        let dir = Dir::open(base);

        if let Ok(mut dir) = dir {
            for (i, filename) in FILENAMES.iter().enumerate() {
                let metadata = if let Ok(metadata) = dir.fstat(filename.as_bytes()) {
                    hits += 1;
                    metadata
                } else {
                    t!("{:02x}: No such file or directory.", i);

                    misses += 1;

                    if let Some(revert_to_readir) = revert_to_readir {
                        if revert_to_readir == misses && hits <= 1 {
                            t!("Too many misses; switching to readdir implementation.");
                            return self.tag_readdir_unix();
                        }
                    }

                    continue;
                };

                if ! metadata.is_dir() {
                    t!("{:02x}: Not a directory.");
                    continue;
                }

                let tag = Tag::from(&metadata);
                t!("{:02x} => Tag({:x})", i, tag.0);

                composite.0[i] = tag
            }
        }

        composite.compress()
    }

    /// Turns a fingerprint into a path in the store.
    ///
    /// [The transformation from a fingerprint to a path] is defined
    /// by the standard.
    ///
    ///   [The transformation from a fingerprint to a path]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#section-3.2.1
    pub fn get_path_by_fingerprint(&self, fingerprint: &str) -> Result<PathBuf> {
        if ! [pgp::FINGERPRINT_LEN_CHARS_V4,
              pgp::FINGERPRINT_LEN_CHARS_V6].contains(&fingerprint.len()) {
            return Err(Error::BadName);
        }
        if fingerprint.chars().any(|c| !c.is_ascii_hexdigit()) {
            return Err(Error::BadName);
        }
        let fingerprint = fingerprint.to_ascii_lowercase();
        Ok(self.base.join(&fingerprint[..2]).join(&fingerprint[2..]))
    }

    /// Turns a path in the store into a fingerprint, if it conforms to the cert-d
    /// specification.
    fn get_fingerprint_by_path(
        &self,
        path: &Path,
    ) -> std::result::Result<String, InternalError> {
        let path = if path.is_absolute() {
            path.strip_prefix(&self.base)
                .map_err(|_| InternalError::PathNotInStore)?
        } else {
            path
        };
        if !self.base.join(path).is_file() {
            return Err(InternalError::BadFingerprintPath);
        }
        if path.components().count() != 2 {
            return Err(InternalError::BadFingerprintPath);
        }
        let components =
            path.components().map(|c| c.as_os_str()).collect::<Vec<_>>();
        if components.iter().any(|c| !c.is_ascii()) {
            return Err(InternalError::BadFingerprintPath);
        }
        let head = components[0].to_string_lossy();
        if head.len() != PATH_PREFIX_LEN {
            return Err(InternalError::BadFingerprintPath);
        }
        let tail = components[1].to_string_lossy();
        if tail.len() != pgp::FINGERPRINT_LEN_CHARS_V4 - PATH_PREFIX_LEN
            && tail.len() != pgp::FINGERPRINT_LEN_CHARS_V6 - PATH_PREFIX_LEN
        {
            return Err(InternalError::BadFingerprintPath);
        }
        Ok(head.to_string() + &tail)
    }

    /// Turns a special name into a path in the store.
    ///
    /// The specification currently defines one [special name].
    /// Non-standard special names are allowed, but they must MUST
    /// start with an underscore, which SHOULD be immediately followed
    /// by the vendor's name, e.g., `_sequoia_some_special.pgp`.
    /// Other names cause this function to return [`Error::BadName`].
    ///
    ///   [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names
    pub fn get_path_by_special(&self, special: &str) -> Result<PathBuf> {
        Self::get_relative_path_by_special(special).map(|special| {
            self.base.join(special)
        })
    }

    /// Returns whether the special name is valid.
    ///
    /// The specification currently defines one [special name].
    /// Non-standard special names are allowed, but they must MUST
    /// start with an underscore, which SHOULD be immediately followed
    /// by the vendor's name, e.g., `_sequoia_some_special.pgp`.
    /// Other names cause this function to return [`Error::BadName`].
    ///
    ///   [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names
    pub fn is_special(special: &str) -> Result<()> {
        Self::get_relative_path_by_special(special).map(|_| ())
    }

    // The meat behind `CertD::get_path_by_special` and
    // `CertD::is_special`.  See them for documentation.
    fn get_relative_path_by_special(special: &str) -> Result<PathBuf> {
        if let Some('_') = special.chars().next() {
            let special = PathBuf::from(special);
            if special.components().count() != 1 {
                Err(Error::BadName)
            } else {
                Ok(special)
            }
        } else if SPECIAL_NAMES.binary_search(&special).is_ok() {
            Ok(PathBuf::from(special))
        } else {
            Err(Error::BadName)
        }
    }

    /// Looks up a certificate in the store by name, i.e., by
    /// fingerprint or by special name.
    ///
    /// The specification currently defines one [special name].
    /// Non-standard special names are allowed, but they must MUST
    /// start with an underscore, which SHOULD be immediately followed
    /// by the vendor's name, e.g., `_sequoia_some_special.pgp`.
    /// Other names cause this function to return [`Error::BadName`].
    ///
    ///   [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names
    ///
    /// If the certificate exists, this function computes the tag,
    /// reads in the certificate data, and returns `Ok(Some((tag,
    /// cert_data)))`.  See [`Tag`] for how the tag can be used to
    /// cache lookups.
    ///
    /// If the certificate does not exist, this function returns `Ok(None)`.
    ///
    /// If an I/O error occurs, or the name was invalid, this function returns
    /// an [`Error`].
    ///
    /// [`CertD::get_file`] is often more efficient if you don't
    /// necessarily need the tag or the file's contents.
    pub fn get(&self, name: &str) -> Result<Option<(Tag, Vec<u8>)>> {
        if let Some(mut fp) = self.get_file(name)? {
            let tag = Tag::try_from(&fp)?;
            let mut buf = Vec::new();
            fp.read_to_end(&mut buf)?;
            Ok(Some((tag, buf)))
        } else {
            Ok(None)
        }
    }

    /// Looks up a certificate in the store by name, i.e., by
    /// fingerprint or by special name.
    ///
    /// The specification currently defines one [special name].
    /// Non-standard special names are allowed, but they must MUST
    /// start with an underscore, which SHOULD be immediately followed
    /// by the vendor's name, e.g., `_sequoia_some_special.pgp`.
    /// Other names cause this function to return [`Error::BadName`].
    ///
    ///   [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names
    ///
    /// If the certificate exists, this function returns
    /// `Ok(Some([std::fs::File]))`.  If the certificate does not
    /// exist, it returns `Ok(None)`.  If an I/O error occurs, or the
    /// name was invalid, this function returns an [`Error`].
    ///
    /// You can get the file's [`Tag`] by doing:
    /// `Tag::try_from(&file)`.  See [`Tag`] for how the tag can be
    /// used to cache lookups.
    pub fn get_file(&self, name: &str) -> Result<Option<File>> {
        let path = self.get_path(name)?;
        match fs::File::open(path) {
            Ok(f) => Ok(Some(f)),
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
            Err(e) => Err(e.into()),
        }
    }

    /// Conditionally looks up a certificate in the store by an name,
    /// i.e. a fingerprint or a special name.
    ///
    /// The specification currently defines one [special name].
    /// Non-standard special names are allowed, but they must MUST
    /// start with an underscore, which SHOULD be immediately followed
    /// by the vendor's name, e.g., `_sequoia_some_special.pgp`.
    /// Other names cause this function to return [`Error::BadName`].
    ///
    ///   [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names
    ///
    /// If the certificate has changed, i.e., the provided tag does
    /// not match the current tag, this function returns
    /// `Ok(Some((cert, tag)))`.  The tag can be used in subsequent
    /// calls to this function.
    ///
    /// If the certificate has not changed, i.e., the provided tag
    /// matches the current tag, or the certificate does not exist,
    /// this function returns `Ok(None)`.
    ///
    /// If an I/O error occurs, or the name was invalid, this function returns
    /// an [`Error`].
    pub fn get_if_changed(
        &self,
        since: Tag,
        name: &str,
    ) -> Result<Option<(Tag, Vec<u8>)>> {
        let path = self.get_path(name)?;
        match fs::File::open(path) {
            Ok(mut f) => {
                let tag = f.metadata()?.try_into()?;
                if since == tag {
                    Ok(None) // Not modified.
                } else {
                    let mut buf = Vec::new();
                    f.read_to_end(&mut buf)?;
                    Ok(Some((tag, buf)))
                }
            }
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
            Err(e) => Err(e.into()),
        }
    }

    /// Get the path to a certificate by fingerprint or special name.
    ///
    /// The specification currently defines one [special name].
    /// Non-standard special names are allowed, but they must MUST
    /// start with an underscore, which SHOULD be immediately followed
    /// by the vendor's name, e.g., `_sequoia_some_special.pgp`.
    /// Other names cause this function to return [`Error::BadName`].
    ///
    ///   [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names
    pub fn get_path(&self, name: &str) -> Result<PathBuf> {
        // Try to convert the name to a path, first as a fingerprint and
        // if that fails as a special name.
        // If the errors get more insightful than just Error::BadName, prefer
        // returning the one from fingerprint_to_path.
        self.get_path_by_fingerprint(name)
            .or_else(|_| self.get_path_by_special(name))
    }

    /// Inserts or updates a cert.
    ///
    /// Requires the fingerprint and a callback function.  The
    /// callback is passed `data`, and an `Option<Vec<u8>>`, which
    /// contains the existing cert data, if any.  The callback is
    /// expected to merge the two copies of the certificate together.
    /// The returned data is written to the store.  Note: The callback
    /// may decide to omit (parts of) the existing data, but this
    /// should be done with great care as not to lose any vital
    /// information.
    ///
    /// The new [`Tag`] is returned, and if `return_inserted` is true,
    /// the data written to the store is also returned.
    ///
    /// This function locks store, which may block the current thread.
    /// Use [`CertD::try_insert`] to avoid blocking.
    pub fn insert<'a, D, M>(&self, fingerprint: &str, data: D,
                            return_inserted: bool, merge: M)
        -> Result<(Tag, Option<Vec<u8>>)>
    where
        M: FnOnce(D, Option<&[u8]>) -> Result<MergeResult<'a>>,
    {
        self.insert_extended(
            fingerprint, data, return_inserted,
            |d| Ok(d),
            merge,
            |r| Ok(r))
    }

    /// Inserts or updates a cert, extended version.
    ///
    /// This function is like [`CertD::insert`], but after obtaining
    /// the lock on the cert-d, and before doing anything else, it
    /// calls `pre`.  Similarly, just before dropping the lock, it
    /// calls `post`.
    ///
    /// If the `pre` callback returns an error, the operation is
    /// aborted, and the error is propagated to the caller.  The
    /// `post` callback can also transform the result.
    ///
    /// The caller can use this functionality to get the cert-d's tag
    /// prior to and after an insert operation, for instance.
    pub fn insert_extended<'a, D, PRE, M, POST>(
        &self, fingerprint: &str, data: D,
        return_inserted: bool, pre: PRE, merge: M, post: POST)
        -> Result<(Tag, Option<Vec<u8>>)>
    where
        PRE: FnOnce(D) -> Result<D>,
        M: FnOnce(D, Option<&[u8]>) -> Result<MergeResult<'a>>,
        POST: FnOnce((Tag, Option<Vec<u8>>)) -> Result<(Tag, Option<Vec<u8>>)>,
    {
        let blocking = true;
        self.insert_impl(
            fingerprint, true, data, return_inserted,
            pre, merge, post,
            blocking)
    }

    /// Inserts or updates a cert, non-blocking variant.
    ///
    /// Requires the fingerprint and a callback function.  The
    /// callback is passed `data`, and an `Option<Vec<u8>>`, which
    /// contains the existing cert data, if any.  The callback is
    /// expected to merge the two copies of the certificate together.
    /// The returned data is written to the store.  Note: The callback
    /// may decide to omit (parts of) the existing data, but this
    /// should be done with great care as not to lose any vital
    /// information.
    ///
    /// The new [`Tag`] is returned, and if `return_inserted` is true,
    /// the data written to the store is also returned.
    ///
    /// This function attempts to lock the store.  If the store is
    /// already locked, then it instead returns [`Error::IoError`]
    /// with an [`std::io::ErrorKind::WouldBlock`].
    pub fn try_insert<'a, D, M>(&self, fingerprint: &str, data: D,
                                return_inserted: bool, merge: M)
        -> Result<(Tag, Option<Vec<u8>>)>
    where
        M: FnOnce(D, Option<&[u8]>) -> Result<MergeResult<'a>>,
    {
        self.try_insert_extended(
            fingerprint, data, return_inserted,
            |d| Ok(d),
            merge,
            |r| Ok(r))
    }

    /// Inserts or updates a cert, extended version.
    ///
    /// This function is like [`CertD::try_insert`], but after
    /// obtaining the lock on the cert-d, and before doing anything
    /// else, it calls `pre`.  Similarly, just before dropping the
    /// lock, it calls `post`.
    ///
    /// If the `pre` callback returns an error, the operation is
    /// aborted, and the error is propagated to the caller.  The
    /// `post` callback can also transform the result.
    ///
    /// The caller can use this functionality to get the cert-d's tag
    /// prior to and after an insert operation, for instance.
    pub fn try_insert_extended<'a, D, PRE, M, POST>(
        &self, fingerprint: &str, data: D,
        return_inserted: bool, pre: PRE, merge: M, post: POST)
        -> Result<(Tag, Option<Vec<u8>>)>
    where
        PRE: FnOnce(D) -> Result<D>,
        M: FnOnce(D, Option<&[u8]>) -> Result<MergeResult<'a>>,
        POST: FnOnce((Tag, Option<Vec<u8>>)) -> Result<(Tag, Option<Vec<u8>>)>,
    {
        let blocking = false;
        self.insert_impl(
            fingerprint, true, data, return_inserted,
            pre, merge, post, blocking)
    }

    /// Inserts or updates a cert.
    ///
    /// Requires the new certificate data, and a callback function.
    /// The fingerprint is extracted from `data`.  The callback is
    /// passed `data`, and an `Option<Vec<u8>>`, which contains the
    /// existing cert data, if any.  The callback is expected to merge
    /// the two copies of the certificate together.  The returned data
    /// is written to the store.  Note: The callback may decide to
    /// omit (parts of) the existing data, but this should be done
    /// with great care as not to lose any vital information.
    ///
    /// The new [`Tag`] is returned, and if `return_inserted` is true,
    /// the data written to the store is also returned.
    ///
    /// This function locks store, which may block the current thread.
    /// Use [`CertD::try_insert_data`] to avoid blocking.
    pub fn insert_data<'a, M>(&self, data: &'a [u8],
                              return_inserted: bool, merge: M)
        -> Result<(Tag, Option<Vec<u8>>)>
    where
        M: FnOnce(&'a [u8], Option<&[u8]>) -> Result<MergeResult<'a>>,
    {
        self.insert_data_extended(
            data, return_inserted,
            |d| Ok(d),
            merge,
            |r| Ok(r))
    }

    /// Inserts or updates a cert, extended version.
    ///
    /// This function is like [`CertD::insert_data`], but after
    /// obtaining the lock on the cert-d, and before doing anything
    /// else, it calls `pre`.  Similarly, just before dropping the
    /// lock, it calls `post`.
    ///
    /// If the `pre` callback returns an error, the operation is
    /// aborted, and the error is propagated to the caller.  The
    /// `post` callback can also transform the result.
    pub fn insert_data_extended<'a, PRE, M, POST>(
        &self, data: &'a [u8],
        return_inserted: bool,
        pre: PRE, merge: M, post: POST)
        -> Result<(Tag, Option<Vec<u8>>)>
    where
        PRE: FnOnce(&'a [u8]) -> Result<&'a [u8]>,
        M: FnOnce(&'a [u8], Option<&[u8]>) -> Result<MergeResult<'a>>,
        POST: FnOnce((Tag, Option<Vec<u8>>)) -> Result<(Tag, Option<Vec<u8>>)>,
    {
        let blocking = true;
        let fingerprint = pgp::fingerprint(data)?;
        self.insert_impl(
            &fingerprint, true, data, return_inserted,
            pre, merge, post,
            blocking)
    }

    /// Inserts or updates a cert, non-blocking variant.
    ///
    /// Requires the new certificate data, and a callback function.
    /// The fingerprint is extracted from `data`.  The callback is
    /// passed `data`, and an `Option<Vec<u8>>`, which contains the
    /// existing cert data, if any.  The callback is expected to merge
    /// the two copies of the certificate together.  The returned data
    /// is written to the store.  Note: The callback may decide to
    /// omit (parts of) the existing data, but this should be done
    /// with great care as not to lose any vital information.
    ///
    /// The new [`Tag`] is returned, and if `return_inserted` is true,
    /// the data written to the store is also returned.
    ///
    /// This function attempts to lock the store.  If the store is
    /// already locked, then it instead returns [`Error::IoError`]
    /// with an [`std::io::ErrorKind::WouldBlock`].
    pub fn try_insert_data<'a, M>(&self, data: &'a [u8],
                                  return_inserted: bool, merge: M)
        -> Result<(Tag, Option<Vec<u8>>)>
    where
        M: FnOnce(&'a [u8], Option<&[u8]>) -> Result<MergeResult<'a>>,
    {
        self.try_insert_data_extended(
            data, return_inserted,
            |d| Ok(d),
            merge,
            |r| Ok(r))
    }

    /// Inserts or updates a cert, non-blocking variant, extended
    /// version.
    ///
    /// This function is like [`CertD::try_insert_data`], but after
    /// obtaining the lock on the cert-d, and before doing anything
    /// else, it calls `pre`.  Similarly, just before dropping the
    /// lock, it calls `post`.
    ///
    /// If the `pre` callback returns an error, the operation is
    /// aborted, and the error is propagated to the caller.  The
    /// `post` callback can also transform the result.
    pub fn try_insert_data_extended<'a, PRE, M, POST>(
        &self, data: &'a [u8],
        return_inserted: bool,
        pre: PRE, merge: M, post: POST)
        -> Result<(Tag, Option<Vec<u8>>)>
    where
        PRE: FnOnce(&'a [u8]) -> Result<&'a [u8]>,
        M: FnOnce(&'a [u8], Option<&[u8]>) -> Result<MergeResult<'a>>,
        POST: FnOnce((Tag, Option<Vec<u8>>)) -> Result<(Tag, Option<Vec<u8>>)>,
    {
        let blocking = false;
        pgp::plausible_tsk_or_tpk(data)?;
        let fingerprint = pgp::fingerprint(data)?;
        self.insert_impl(
            &fingerprint, true, data, return_inserted,
            pre, merge, post,
            blocking)
    }

    /// Inserts or updates the cert or key stored under a special name.
    ///
    /// Requires the special name, the cert or key in binary format
    /// and a callback function.  The callback is invoked with an
    /// `Option<Vec<u8>>` of the existing data (if any), and is
    /// expected to merge the two copies together.  The returned
    /// `Vec<u8>` is written to the store under the special name.
    /// (Note: The function may decide to omit (parts of) the existing
    /// data, but this should be done with great care as not to lose
    /// any vital information.)  The new [`Tag`] is returned, and if
    /// `return_inserted` is true, the data written to the store is
    /// also returned.  Otherwise, `None` is returned.
    ///
    /// This function locks store, which may block the current thread.
    /// Use [`CertD::try_insert_special`] to avoid blocking.
    ///
    /// The specification currently defines one [special name].
    /// Non-standard special names are allowed, but they must MUST
    /// start with an underscore, which SHOULD be immediately followed
    /// by the vendor's name, e.g., `_sequoia_some_special.pgp`.
    /// Other names cause this function to return [`Error::BadName`].
    ///
    ///   [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names
    pub fn insert_special<'a, D, M>(
        &self,
        special_name: &str,
        data: D,
        return_inserted: bool,
        merge: M,
    ) -> Result<(Tag, Option<Vec<u8>>)>
    where
        M: FnOnce(D, Option<&[u8]>) -> Result<MergeResult<'a>>,
    {
        self.insert_special_extended(
            special_name, data, return_inserted,
            |d| Ok(d),
            merge,
            |r| Ok(r))
    }

    /// Inserts or updates the cert or key stored under a special
    /// name, extended version.
    ///
    /// This function is like [`CertD::insert_special`], but after
    /// obtaining the lock on the cert-d, and before doing anything
    /// else, it calls `pre`.  Similarly, just before dropping the
    /// lock, it calls `post`.
    ///
    /// If the `pre` callback returns an error, the operation is
    /// aborted, and the error is propagated to the caller.  The
    /// `post` callback can also transform the result.
    ///
    /// The caller can use this functionality to get the cert-d's tag
    /// prior to and after an insert operation, for instance.
    pub fn insert_special_extended<'a, D, PRE, M, POST>(
        &self,
        special_name: &str,
        data: D,
        return_inserted: bool,
        pre: PRE, merge: M, post: POST
    ) -> Result<(Tag, Option<Vec<u8>>)>
    where
        PRE: FnOnce(D) -> Result<D>,
        M: FnOnce(D, Option<&[u8]>) -> Result<MergeResult<'a>>,
        POST: FnOnce((Tag, Option<Vec<u8>>)) -> Result<(Tag, Option<Vec<u8>>)>,
    {
        let blocking = true;
        self.insert_impl(
            special_name, false, data, return_inserted,
            pre,
            merge,
            post,
            blocking)
    }

    /// Inserts or updates the cert or key stored under a special
    /// name, non-blocking variant.
    ///
    /// Requires the special name, the cert or key in binary format
    /// and a callback function.  The callback is invoked with an
    /// `Option<Vec<u8>>` of the existing data (if any), and is
    /// expected to merge the two copies together.  The returned
    /// `Vec<u8>` is written to the store under the special name.
    /// (Note: The function may decide to omit (parts of) the existing
    /// data, but this should be done with great care as not to lose
    /// any vital information.)  The new [`Tag`] is returned, and if
    /// `return_inserted` is true, the data written to the store is
    /// also returned.  Otherwise, `None` is returned.
    ///
    /// This function attempts to lock the store.  If the store is
    /// already locked, then it instead returns [`Error::IoError`]
    /// with an [`std::io::ErrorKind::WouldBlock`].
    ///
    /// The specification currently defines one [special name].
    /// Non-standard special names are allowed, but they must MUST
    /// start with an underscore, which SHOULD be immediately followed
    /// by the vendor's name, e.g., `_sequoia_some_special.pgp`.
    /// Other names cause this function to return [`Error::BadName`].
    ///
    ///   [special name]: https://www.ietf.org/archive/id/draft-nwjw-openpgp-cert-d-00.html#name-special-names
    pub fn try_insert_special<'a, D, M>(
        &self,
        special_name: &str,
        data: D,
        return_inserted: bool,
        merge: M,
    ) -> Result<(Tag, Option<Vec<u8>>)>
    where
        M: FnOnce(D, Option<&[u8]>) -> Result<MergeResult<'a>>,
    {
        self.try_insert_special_extended(
            special_name, data, return_inserted,
            |d| Ok(d),
            merge,
            |r| Ok(r))
    }

    /// Inserts or updates the cert or key stored under a special
    /// name, non-blocking variant, extended version.
    ///
    /// This function is like [`CertD::try_insert_special`], but after
    /// obtaining the lock on the cert-d, and before doing anything
    /// else, it calls `pre`.  Similarly, just before dropping the
    /// lock, it calls `post`.
    ///
    /// If the `pre` callback returns an error, the operation is
    /// aborted, and the error is propagated to the caller.  The
    /// `post` callback can also transform the result.
    ///
    /// The caller can use this functionality to get the cert-d's tag
    /// prior to and after an insert operation, for instance.
    pub fn try_insert_special_extended<'a, D, PRE, M, POST>(
        &self,
        special_name: &str,
        data: D,
        return_inserted: bool,
        pre: PRE, merge: M, post: POST
    ) -> Result<(Tag, Option<Vec<u8>>)>
    where
        PRE: FnOnce(D) -> Result<D>,
        M: FnOnce(D, Option<&[u8]>) -> Result<MergeResult<'a>>,
        POST: FnOnce((Tag, Option<Vec<u8>>)) -> Result<(Tag, Option<Vec<u8>>)>,
    {
        let blocking = false;
        self.insert_impl(
            special_name, false, data, return_inserted,
            pre,
            merge,
            post,
            blocking)
    }

    fn insert_impl<'a, D, PRE, M, POST>(
        &self,
        name: &str, name_is_fingerprint: bool,
        data: D,
        return_inserted: bool,
        pre: PRE,
        merge: M,
        post: POST,
        blocking: bool,
    ) -> Result<(Tag, Option<Vec<u8>>)>
    where
        PRE: FnOnce(D) -> Result<D>,
        M: FnOnce(D, Option<&[u8]>) -> Result<MergeResult<'a>>,
        POST: FnOnce((Tag, Option<Vec<u8>>)) -> Result<(Tag, Option<Vec<u8>>)>,
    {
        let name = if name_is_fingerprint {
            pgp::canonicalize_fingerprint(name)?
        } else {
            Cow::Borrowed(name)
        };

        let target_path = self.get_path(&name)?;

        let mut lf = RwLock::new(self.idempotent_create_lockfile()?);
        // Lock exclusively
        let lock = if blocking {
            lf.write()?
        } else {
            lf.try_write()?
        };

        // Let the caller examine the cert-d with the lock.
        let data = pre(data)?;

        // Make sure the directory exists.
        fs::create_dir_all(target_path.parent().expect("at least one leg"))?;

        let old_cert = self.get(&name)?.map(|(_, cert)| cert);
        let old_cert = old_cert.as_deref();
        let merge_result = merge(data, old_cert)?;
        let new_cert = match merge_result {
            MergeResult::Keep => old_cert.unwrap_or(&[]),
            MergeResult::DataRef(data) => data,
            MergeResult::Data(ref data) => data,
        };

        pgp::plausible_tsk_or_tpk(new_cert)?;

        if name_is_fingerprint {
            let fingerprint = pgp::fingerprint(new_cert)?;
            if fingerprint != name {
                return Err(Error::BadData(pgp::Error::WrongCertificate(
                    name.to_string(), fingerprint.to_string())));
            }
        }

        if let MergeResult::Keep = merge_result {
            // There's nothing to do.
        } else {
            let mut tmp = NamedTempFile::new_in(&self.base)?;
            tmp.write_all(new_cert.as_ref())?;
            tmp.persist(&target_path).map_err(|e| e.error)?;
        }

        let tag = fs::File::open(&target_path)?.metadata()?.try_into()?;

        let cert = if return_inserted {
            Some(new_cert.to_vec())
        } else {
            None
        };

        // Let the caller examine the result with the cert-d lock.
        let (tag, cert) = post((tag, cert))?;

        drop(lock);

        Ok((tag, cert))
    }

    /// Iterates over the certs in the store returning their fingerprints.
    ///
    /// Note: this only considers certificates that are stored under
    /// their fingerprint; it does not include certificates stored
    /// under a special name.
    pub fn fingerprints(&self) -> impl Iterator<Item = Result<String>> + '_ {
        WalkDir::new(&self.base)
            // take only subdirs of depth 2
            .max_depth(2)
            .min_depth(2)
            .into_iter()
            // Convert the paths to fingerprints. The store is a shared
            // directory, writable by anyone, so there may be files that don't
            // correspond to a fingerprint. Filter them out.
            .filter_map(move |e| match e {
                Ok(entry) => match self.get_fingerprint_by_path(entry.path()) {
                    Ok(fingerprint) => Some(Ok(fingerprint)),
                    Err(_) => None,
                },
                Err(err) => {
                    if let Some(std::io::ErrorKind::NotFound)
                        = err.io_error().map(|err| err.kind())
                    {
                        // Ignore file not found.
                        None
                    } else {
                        Some(Err(err.into()))
                    }
                }
            })
    }

    /// Iterates over the certs in the store.
    ///
    /// Iterates over the certs in the store returning the
    /// fingerprint, and a file handle for each cert.
    ///
    /// Note: this only considers certificates that are stored under
    /// their fingerprint; it does not include certificates stored
    /// under a special name.
    pub fn iter_files(
        &self,
    ) -> impl Iterator<Item = Result<(String, File)>> + '_ {
        // Helper function analogous to get, with the fingerprint included in
        // the output.
        let get_with_fingerprint = move |fingerprint: &str| -> Result<(String, File)> {
            match self.get_file(fingerprint)? {
                None => Err(Error::IoError(io::Error::new(
                    io::ErrorKind::Other,
                    // The file was found when fingerprints() walked over the
                    // directory, but wasn't found for reading now.
                    format!("The file for {} disappeared.", fingerprint),
                ))),
                Some(file) => Ok((fingerprint.to_owned(), file)),
            }
        };

        self.fingerprints()
            .map(move |fingerprint_result| {
                fingerprint_result.and_then(|fingerprint| {
                    get_with_fingerprint(&fingerprint)
                })
            })
    }

    /// Iterates over the certs in the store.
    ///
    /// Iterates over the certs in the store returning the
    /// fingerprint, the tag, and the data for each cert.
    ///
    /// Note: this only considers certificates that are stored under
    /// their fingerprint; it does not include certificates stored
    /// under a special name.
    pub fn iter(
        &self,
    ) -> impl Iterator<Item = Result<(String, Tag, Vec<u8>)>> + '_ {
        self.iter_files()
            .map(|r| {
                let (fingerprint, mut fp) = r?;
                let tag = Tag::try_from(&fp)?;

                let mut data = Vec::new();
                fp.read_to_end(&mut data)?;

                Ok((fingerprint, tag, data))
            })
    }

    fn idempotent_create_lockfile(&self) -> Result<std::fs::File> {
        let lock_path = self.base.join("writelock");
        // Open the lockfile for writing, and create it if it does not exist yet.
        std::fs::OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .open(lock_path)
            .map_err(Into::into)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use assert_fs::prelude::*;
    use predicates::prelude::*;

    use crate::TRUST_ROOT;

    fn test_base() -> assert_fs::TempDir {
        let base = assert_fs::TempDir::new().unwrap();
        match std::env::var_os("CERTD_TEST_PERSIST") {
            Some(_) => {
                eprintln!("Test base dir: {}", &base.path().to_string_lossy());
                base.into_persistent()
            }
            None => base,
        }
    }

    struct Testdata<'a> {
        data: &'a [u8],
        fingerprint: &'a str,
    }

    impl Testdata<'_> {
        fn path(&self) -> String {
            [&self.fingerprint[..2], &self.fingerprint[2..]].join("/")
        }

        fn add_to_certd(&self, base: &assert_fs::TempDir) {
            base.child(self.path()).write_binary(self.data).unwrap();
        }
    }

    static ALICE: Testdata = Testdata {
        fingerprint: "eb85bb5fa33a75e15e944e63f231550c4f47e38e",
        data: include_bytes!("../../testdata/alice.pgp"),
    };

    static BOB: Testdata = Testdata {
        fingerprint: "d1a66e1a23b182c9980f788cfbfcc82a015e7330",
        data: include_bytes!("../../testdata/bob.pgp"),
    };

    static TESTY: Testdata = Testdata {
        fingerprint: "39d100ab67d5bd8c04010205fb3751f1587daef1",
        data: include_bytes!("../../testdata/testy-new.pgp"),
    };

    fn setup_testdir(
        testdata: &[&Testdata],
    ) -> Result<(assert_fs::TempDir, CertD)> {
        let base = test_base();
        for t in testdata.iter() {
            t.add_to_certd(&base);
        }

        let trust_root_data = include_bytes!("../../testdata/sender.pgp");
        base.child("trust-root")
            .write_binary(trust_root_data)
            .unwrap();

        let certd = CertD::with_base_dir(&base)?;
        Ok((base, certd))
    }

    #[test]
    fn get_fingerprint() -> std::result::Result<(), Box<dyn std::error::Error>> {
        let data = include_bytes!("../../testdata/testy-new.pgp");

        let base = test_base();
        base.child("39/d100ab67d5bd8c04010205fb3751f1587daef1")
            .write_binary(data)
            .unwrap();

        let certd = CertD::with_base_dir(&base)?;

        let (tag, cert) = certd
            .get("39d100ab67d5bd8c04010205fb3751f1587daef1")?
            .unwrap();
        assert_eq!(cert, data);

        assert!(certd
            .get_if_changed(tag, "39d100ab67d5bd8c04010205fb3751f1587daef1")?
            .is_none());

        let mut fp = certd
            .get_file("39d100ab67d5bd8c04010205fb3751f1587daef1")?
            .unwrap();
        let tag = Tag::try_from(&fp)?;
        let mut data = Vec::new();
        fp.read_to_end(&mut data)?;
        assert_eq!(cert, data);

        assert!(certd
            .get_if_changed(tag, "39d100ab67d5bd8c04010205fb3751f1587daef1")?
            .is_none());

        base.close().unwrap();
        Ok(())
    }

    #[test]
    fn get_special() -> std::result::Result<(), Box<dyn std::error::Error>> {
        let data = include_bytes!("../../testdata/sender.pgp");

        let base = test_base();
        base.child("trust-root").write_binary(data).unwrap();

        let certd = CertD::with_base_dir(&base)?;

        let (tag, cert) = certd.get(TRUST_ROOT)?.unwrap();
        assert_eq!(cert, data);

        assert!(certd.get_if_changed(tag, TRUST_ROOT)?.is_none());

        base.close().unwrap();
        Ok(())
    }

    #[test]
    fn get_not_found() -> Result<()> {
        let base = test_base();
        let certd = CertD::with_base_dir(&base)?;
        let result = certd.get("39d100ab67d5bd8c04010205fb3751f1587daef1");
        assert!(matches!(result, Ok(None)));
        Ok(())
    }

    #[test]
    fn insert_locked() -> Result<()> {
        let data = include_bytes!("../../testdata/testy-new.pgp");
        let base = test_base();

        let file = base.child("39/d100ab67d5bd8c04010205fb3751f1587daef1");
        file.assert(predicate::path::missing());

        let certd = CertD::with_base_dir(&base)?;

        // Lock the lockfile before we try to insert
        let mut lf = RwLock::new(certd.idempotent_create_lockfile()?);
        // Lock exclusively
        let _lock = lf.write()?;

        let result = certd.try_insert_data(
            data, false,
            |new: &[u8], old: Option<&[u8]>| {
                assert!(old.is_none());
                Ok(MergeResult::DataRef(new))
            });

        match result.unwrap_err() {
            Error::IoError(e) if e.kind() == io::ErrorKind::WouldBlock => {
                Ok(())
            }
            e => Err(e),
        }
    }

    #[test]
    fn insert_special_locked() -> Result<()> {
        let data = include_bytes!("../../testdata/sender.pgp");
        let base = test_base();

        let file = base.child("trust-root");
        file.assert(predicate::path::missing());

        let certd = CertD::with_base_dir(&base)?;

        // Lock the lockfile before we try to insert
        let mut lock = RwLock::new(certd.idempotent_create_lockfile()?);
        // Lock exclusively
        let _lock = lock.write()?;

        let result = certd.try_insert_special(
            TRUST_ROOT,
            &data[..],
            false,
            |new: &[u8], old: Option<&[u8]>| {
                assert!(old.is_none());
                Ok(MergeResult::DataRef(new))
            },
        );

        match result.unwrap_err() {
            Error::IoError(e) if e.kind() == io::ErrorKind::WouldBlock => {
                Ok(())
            }
            e => Err(e),
        }
    }

    #[test]
    fn insert_new() -> Result<()> {
        let data = include_bytes!("../../testdata/testy-new.pgp");
        let data = &data[..];
        let base = test_base();

        let file = base.child("39/d100ab67d5bd8c04010205fb3751f1587daef1");
        file.assert(predicate::path::missing());

        let certd = CertD::with_base_dir(&base)?;

        let (_, inserted) = certd.insert_data(
            data,
            true,
            |new: &[u8], old: Option<&[u8]>| {
                assert!(old.is_none());
                Ok(MergeResult::DataRef(new))
            })?;
        file.assert(data);
        assert_eq!(inserted.as_deref(), Some(data));

        Ok(())
    }

    #[test]
    fn insert_special_new() -> Result<()> {
        let data = include_bytes!("../../testdata/sender.pgp");
        let data = &data[..];
        let base = test_base();

        let file = base.child("trust-root");
        file.assert(predicate::path::missing());

        let certd = CertD::with_base_dir(&base)?;

        let (_, inserted) = certd.insert_special(
            "trust-root",
            data,
            true,
            |new: &[u8], old: Option<&[u8]>| {
                assert!(old.is_none());
                Ok(MergeResult::DataRef(new))
            },
        )?;
        file.assert(data);
        assert_eq!(inserted.as_deref(), Some(data));

        Ok(())
    }

    #[test]
    fn insert_update() -> std::result::Result<(), Box<dyn std::error::Error>> {
        let data = include_bytes!("../../testdata/testy-new.pgp");
        let data = &data[..];
        let base = test_base();

        let file = base.child("39/d100ab67d5bd8c04010205fb3751f1587daef1");
        file.touch().unwrap();
        file.assert(predicate::str::is_empty());

        let certd = CertD::with_base_dir(&base)?;

        let (_, inserted) = certd.insert_data(
            data,
            true,
            |new: &[u8], old: Option<&[u8]>| {
                assert!(old.is_some());
                Ok(MergeResult::DataRef(new))
            })?;
        file.assert(data);
        assert_eq!(inserted.as_deref(), Some(data));

        Ok(())
    }

    #[test]
    fn insert_special_update(
    ) -> std::result::Result<(), Box<dyn std::error::Error>> {
        let data = include_bytes!("../../testdata/sender.pgp");
        let data = &data[..];
        let base = test_base();

        let file = base.child("trust-root");
        file.touch().unwrap();
        file.assert(predicate::str::is_empty());

        let certd = CertD::with_base_dir(&base)?;

        let (_, inserted) = certd.insert_special(
            TRUST_ROOT,
            data,
            true,
            |new: &[u8], old: Option<&[u8]>| {
                assert!(old.is_some());
                Ok(MergeResult::DataRef(new))
            },
        )?;
        file.assert(data);
        assert_eq!(inserted.as_deref(), Some(data));

        Ok(())
    }

    #[test]
    fn insert_get() -> std::result::Result<(), Box<dyn std::error::Error>> {
        let data = include_bytes!("../../testdata/testy-new.pgp");
        let data = &data[..];
        let base = test_base();

        let certd = CertD::with_base_dir(&base)?;

        certd.insert_data(
            data,
            false,
            |new: &[u8], old: Option<&[u8]>| {
                assert!(old.is_none());
                Ok(MergeResult::DataRef(new))
            })?;
        let (_, cert) = certd
            .get("39d100ab67d5bd8c04010205fb3751f1587daef1")?
            .unwrap();
        assert_eq!(cert, data);

        Ok(())
    }

    #[test]
    fn get_path_by_fingerprint() -> Result<()> {
        let base = test_base();
        let certd = CertD::with_base_dir(&base)?;

        let expected = base
            .path()
            .join("39")
            .join("d100ab67d5bd8c04010205fb3751f1587daef1");

        let fingerprint = "39d100ab67d5bd8c04010205fb3751f1587daef1";
        assert_eq!(certd.get_path_by_fingerprint(fingerprint)?, expected);

        let fingerprint = "39D100AB67D5BD8C04010205FB3751F1587DAEF1";
        assert_eq!(certd.get_path_by_fingerprint(fingerprint)?, expected);

        let fingerprint = "39D100ab67D5bD8C04010205FB3751f1587DAeF1";
        assert_eq!(certd.get_path_by_fingerprint(fingerprint)?, expected);

        Ok(())
    }

    #[test]
    fn get_path_by_fingerprint_negative() -> Result<()> {
        let base = test_base();
        let certd = CertD::with_base_dir(&base)?;

        // empty
        let fingerprint = "";
        let result = certd.get_path_by_fingerprint(fingerprint);
        assert!(matches!(result.unwrap_err(), Error::BadName));

        // too short
        let fingerprint = "39d100ab67d5bd8c04010205fb3751f1587daef";
        let result = certd.get_path_by_fingerprint(fingerprint);
        assert!(matches!(result.unwrap_err(), Error::BadName));

        // not ascii hex
        let fingerprint = "peter";
        let result = certd.get_path_by_fingerprint(fingerprint);
        assert!(matches!(result.unwrap_err(), Error::BadName));
        Ok(())
    }

    #[test]
    fn get_path_by_special() -> Result<()> {
        let base = test_base();
        let certd = CertD::with_base_dir(&base)?;

        let expected = base.path().join(TRUST_ROOT);

        let name = "trust-root";
        assert_eq!(certd.get_path_by_special(name)?, expected);

        let name = "_sequoia";
        assert_eq!(certd.get_path_by_special(name)?,
                   base.path().join(name));

        let name = "_sequoia_foo";
        assert_eq!(certd.get_path_by_special(name)?,
                   base.path().join(name));

        Ok(())
    }

    #[test]
    fn get_path_by_special_negative() -> Result<()> {
        let base = test_base();
        let certd = CertD::with_base_dir(&base)?;

        // empty
        let name = "";
        let result = certd.get_path_by_special(name);
        assert!(matches!(result.unwrap_err(), Error::BadName));

        // unknown
        let name = "mySpecialName";
        let result = certd.get_path_by_special(name);
        assert!(matches!(result.unwrap_err(), Error::BadName));

        // Case matters.
        let name = "TRUST-ROOT";
        let result = certd.get_path_by_special(name);
        assert!(matches!(result.unwrap_err(), Error::BadName));

        let name = "TrUsT-RooT";
        let result = certd.get_path_by_special(name);
        assert!(matches!(result.unwrap_err(), Error::BadName));

        // Directories are not currently allowed.
        let name = "_sequoia_foo/bar";
        let result = certd.get_path_by_special(name);
        assert!(matches!(result.unwrap_err(), Error::BadName));

        Ok(())
    }

    #[test]
    fn is_special() -> Result<()> {
        assert!(CertD::is_special("trust-root").is_ok());
        assert!(CertD::is_special("TRUST-ROOT").is_err());

        assert!(CertD::is_special("_special_foo_bar").is_ok());
        assert!(CertD::is_special("special_foo_bar").is_err());

        assert!(CertD::is_special("8f17777118a33dda9ba48e62aacb3243630052d9").is_err());

        Ok(())
    }

    #[test]
    fn fingerprints() -> Result<()> {
        use std::collections::HashSet;

        let (_base, certd) = setup_testdir(&[&ALICE, &BOB, &TESTY])?;

        let iter_fingerprint = certd.fingerprints();
        let fingerprints = iter_fingerprint.collect::<Result<HashSet<_>>>()?;
        let expected: HashSet<_> =
            [ALICE.fingerprint, BOB.fingerprint, TESTY.fingerprint]
                .iter()
                .map(|&s| s.to_owned())
                .collect();
        assert_eq!(expected, fingerprints);

        Ok(())
    }

    #[test]
    fn fingerprints_empty() -> Result<()> {
        use std::collections::HashSet;

        let base = test_base();
        let certd = CertD::with_base_dir(&base)?;

        let iter_fingerprint = certd.fingerprints();
        let fingerprints = iter_fingerprint.collect::<Result<HashSet<_>>>()?;
        assert!(fingerprints.is_empty());

        Ok(())
    }

    #[test]
    fn fingerprints_junk() -> Result<()> {
        use std::collections::HashSet;

        let (base, certd) = setup_testdir(&[&ALICE, &BOB, &TESTY])?;
        base.child("some_file").write_str("some_text").unwrap();
        base.child("aa/some_file").write_str("some_text").unwrap();
        base.child("aa/aa/some_file")
            .write_str("some_text")
            .unwrap();

        let iter_fingerprint = certd.fingerprints();
        let fingerprints = iter_fingerprint.collect::<Result<HashSet<_>>>()?;
        let expected: HashSet<_> =
            [ALICE.fingerprint, BOB.fingerprint, TESTY.fingerprint]
                .iter()
                .map(|&s| s.to_owned())
                .collect();
        assert_eq!(expected, fingerprints);

        Ok(())
    }

    #[test]
    fn iter() -> Result<()> {
        use std::collections::HashSet;

        let (_base, certd) = setup_testdir(&[&ALICE, &BOB, &TESTY])?;

        let mut expected: HashSet<_> = [&ALICE, &BOB, &TESTY]
            .iter()
            .map(|&s| {
                (
                    s.fingerprint.to_owned(),
                    certd.get(s.fingerprint).unwrap().unwrap().0,
                    s.data.to_vec(),
                )
            })
            .collect();

        for item in certd.iter() {
            let item = item?;
            assert!(expected.contains(&item));
            expected.remove(&item);
        }
        assert!(expected.is_empty());

        Ok(())
    }

    #[test]
    fn iter_files() -> Result<()> {
        use std::collections::HashSet;

        let (_base, certd) = setup_testdir(&[&ALICE, &BOB, &TESTY])?;

        let mut expected: HashSet<_> = [&ALICE, &BOB, &TESTY]
            .iter()
            .map(|&s| {
                (
                    s.fingerprint.to_owned(),
                    certd.get(s.fingerprint).unwrap().unwrap().0,
                    s.data.to_vec().into_boxed_slice(),
                )
            })
            .collect();

        for item in certd.iter_files() {
            let (fingerprint, mut fp) = item?;
            let tag = Tag::try_from(&fp)?;

            let mut cert = Vec::new();
            fp.read_to_end(&mut cert)?;

            let item = (fingerprint, tag, cert.into());

            assert!(expected.contains(&item));
            expected.remove(&item);
        }
        assert!(expected.is_empty());

        Ok(())
    }

    #[test]
    fn base_path() -> Result<()> {
        let base = assert_fs::TempDir::new().unwrap();
        let certd = CertD::with_base_dir(&base)?;

        assert_eq!(certd.base_dir(), base.path());
        Ok(())
    }

    #[test]
    fn default_store_path() {
        assert!(CertD::default_store_path().is_ok(),
                "The default store's path is not defined for this platform.");
    }

    #[test]
    fn certd_does_not_exist() -> Result<()> {
        let mut base = assert_fs::TempDir::new().unwrap().path().to_path_buf();
        base.push("asdflkj");

        // std::fs::try_exists would be better, but it is still
        // experimental.
        assert!(std::fs::metadata(&base).is_err());

        let certd = CertD::with_base_dir(&base)?;

        // fingerprints shouldn't fail even if the cert directory did
        // not exist.
        let fingerprints = certd.fingerprints().collect::<Result<Vec<_>>>()?;
        assert_eq!(fingerprints.len(), 0);

        Ok(())
    }

    #[test]
    fn certd_tag() -> Result<()> {
        let (_base, certd) = setup_testdir(&[&ALICE])?;

        let certd_tag = || -> Tag {
            let tag = certd.tag();

            let tag_readdir_std = certd.tag_readdir_std();
            assert_eq!(tag, tag_readdir_std);

            #[cfg(unix)]
            {
                let tag_readdir_unix = certd.tag_readdir_unix();
                assert_eq!(tag, tag_readdir_unix);
            }

            let tag_probe_std = certd.tag_probe_std(None);
            assert_eq!(tag, tag_probe_std);

            #[cfg(unix)]
            {
                let tag_probe_unix = certd.tag_probe_unix(None);
                assert_eq!(tag, tag_probe_unix);
            }

            tag
        };

        let iter_fingerprint = certd.fingerprints();
        let fingerprints = iter_fingerprint.collect::<Result<Vec<_>>>()?;
        assert_eq!(fingerprints.len(), 1);

        let tag0 = certd_tag();
        eprintln!("tag0: {:x}", tag0.0);

        // Insert a new certificate.  This should change the tag.
        eprintln!("Inserting BOB");
        certd.insert_data(
            &BOB.data,
            false,
            |new: &[u8], old: Option<&[u8]>| {
                assert!(old.is_none());
                Ok(MergeResult::DataRef(new))
            })?;

        let tag1 = certd_tag();
        eprintln!("tag1: {:x}", tag1.0);

        assert_ne!(tag0, tag1);

        // Adding a special shouldn't change the certd's tag.
        eprintln!("Inserting special _bob");
        certd.insert_special(
            "_bob",
            BOB.data,
            false,
            |new: &[u8], old: Option<&[u8]>| {
                assert!(old.is_none());
                Ok(MergeResult::DataRef(new))
            })?;

        let tag2 = certd_tag();
        eprintln!("tag2: {:x}", tag2.0);
        assert_eq!(tag1, tag2);

        // Insert a new certificate.  This should change the tag.
        eprintln!("Inserting TESTY");
        certd.insert_data(
            TESTY.data,
            false,
            |new: &[u8], old: Option<&[u8]>| {
                assert!(old.is_none());
                Ok(MergeResult::DataRef(new))
            })?;

        let tag3 = certd_tag();
        eprintln!("tag3: {:x}", tag3.0);
        assert_ne!(tag2, tag3);

        Ok(())
    }
}
