Skip to main content

git_xcrypt/
lib.rs

1//! Logic behind the `git-xcrypt` binary.
2//!
3//! The crate is split into a library and a thin binary so integration tests can
4//! drive the logic directly instead of only through a subprocess.
5//!
6//! Nothing here may write to `stdout`. On the filter path git treats our
7//! `stdout` as the file content itself, so a stray `println!` silently corrupts
8//! a user's file. Diagnostics go to `stderr`.
9
10use thiserror::Error;
11
12pub mod commands;
13pub mod crypto;
14pub mod git;
15pub mod rules;
16pub mod util;
17
18/// Errors returned by library operations.
19///
20/// The variants line up with the exit codes the binary reports, so a caller can
21/// map an error to a code without inspecting its message.
22#[derive(Debug, Error)]
23pub enum Error {
24    /// Reading the input or writing the output failed.
25    #[error("i/o failure: {0}")]
26    Io(#[from] std::io::Error),
27
28    /// The operating system refused to provide randomness.
29    #[error("could not draw randomness from the operating system: {0}")]
30    Entropy(String),
31
32    /// The content is not a file this build can read.
33    #[error("format error: {0}")]
34    Format(String),
35
36    /// The file belongs to a different repository key.
37    #[error(
38        "this file was encrypted with key {}, but the repository holds key {}",
39        hex(wanted),
40        hex(have)
41    )]
42    KeyMismatch {
43        /// Fingerprint the file asks for.
44        wanted: [u8; crypto::format::KEY_ID_LEN],
45        /// Fingerprint we actually hold.
46        have: [u8; crypto::format::KEY_ID_LEN],
47    },
48
49    /// Authentication failed, or the cipher refused the input.
50    #[error("{0}")]
51    Crypto(String),
52
53    /// The repository is not in a state this command can act on.
54    #[error("{0}")]
55    Config(String),
56
57    /// No repository key is present.
58    #[error("no repository key; run `git-xcrypt init` or `git-xcrypt unlock <key-file>`")]
59    NoKey,
60
61    /// The command line asked for something impossible.
62    #[error("{0}")]
63    Usage(String),
64}
65
66impl Error {
67    /// The process exit code this error reports.
68    ///
69    /// Callers map errors to codes here rather than at each call site, so the
70    /// set stays consistent across every command.
71    #[must_use]
72    pub fn exit_code(&self) -> u8 {
73        match self {
74            Self::Usage(_) | Self::Io(_) | Self::Entropy(_) => util::exit::USAGE,
75            Self::Config(_) => util::exit::CONFIG,
76            Self::NoKey => util::exit::NO_KEY,
77            Self::Format(_) | Self::KeyMismatch { .. } | Self::Crypto(_) => util::exit::FORMAT,
78        }
79    }
80}
81
82/// Result alias for library operations.
83pub type Result<T> = std::result::Result<T, Error>;
84
85/// Renders a key fingerprint the way every user-facing message shows it.
86fn hex(bytes: &[u8]) -> String {
87    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
88}
89
90/// Formats a key fingerprint for display.
91#[must_use]
92pub fn format_key_id(key_id: &[u8; crypto::format::KEY_ID_LEN]) -> String {
93    hex(key_id)
94}