use std::fs;
use std::path::{Path, PathBuf};
use bstr::ByteSlice as _;
use crate::crypto::format::looks_encrypted;
use crate::crypto::key::MasterKey;
use crate::crypto::keyfile;
use crate::git::repo::Repo;
use crate::rules::decide::{self, Outcome};
use crate::rules::declaration::EolMode;
use crate::{Error, Result};
pub fn run(path: &Path) -> Result<Outcome> {
let repo = Repo::discover_from_cwd();
if let Ok(repo) = &repo {
refuse_private_path(repo, path)?;
}
let content = fs::read(path).map_err(|err| named_io(path, &err))?;
let key = if looks_encrypted(&content) {
Some(repo?.load_key()?)
} else {
None
};
convert(key.as_ref(), &name_of(path), &content)
}
fn refuse_private_path(repo: &Repo, path: &Path) -> Result<()> {
let Some(target) = resolved(path) else {
return Ok(());
};
for private in [repo.git_dir(), repo.common_dir()] {
if resolved(private).is_some_and(|private| target.starts_with(private)) {
return Err(Error::Config(format!(
"{}: this is inside the git directory, which this command never prints. \
To carry the repository key to another machine, use `git-xcrypt export-key`.",
path.display()
)));
}
}
Ok(())
}
fn resolved(path: &Path) -> Option<PathBuf> {
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir().ok()?.join(path)
};
Some(
fs::canonicalize(&absolute)
.unwrap_or_else(|_| crate::git::repo::lexically_normal(&absolute)),
)
}
fn named_io(path: &Path, err: &std::io::Error) -> Error {
Error::Io(std::io::Error::other(format!(
"{}: could not read it ({err})",
path.display()
)))
}
pub fn convert(key: Option<&MasterKey>, name: &[u8], content: &[u8]) -> Result<Outcome> {
if keyfile::holds_a_key(content) {
return Err(refuse_key(name));
}
if !looks_encrypted(content) {
return Ok(Outcome {
content: content.to_vec(),
warning: None,
});
}
let outcome = decide::smudge(key, name, content, false, Some(EolMode::Lf), None, None)?;
if keyfile::holds_a_key(&outcome.content) {
drop(zeroize::Zeroizing::new(outcome.content));
return Err(refuse_key(name));
}
Ok(outcome)
}
fn refuse_key(name: &[u8]) -> Error {
Error::Config(format!(
"{}: this is a git-xcrypt key file, and a key is never printed. \
To carry it to another machine, use `git-xcrypt export-key`.",
name.as_bstr()
))
}
fn name_of(path: &Path) -> Vec<u8> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt as _;
path.as_os_str().as_bytes().to_vec()
}
#[cfg(not(unix))]
{
path.to_string_lossy().replace('\\', "/").into_bytes()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::key::MASTER_KEY_LEN;
fn key() -> MasterKey {
MasterKey::from_bytes([5u8; MASTER_KEY_LEN])
}
#[test]
fn a_key_file_is_refused_although_it_carries_no_data_magic() {
let exported = crate::crypto::keyfile::encode_portable(&key());
for content in [exported.as_bytes(), b"\0GITXCRYPTKEY\0\x01somekeymaterial"] {
let error = convert(Some(&key()), b"notes.txt", content).expect_err("must refuse");
assert_eq!(error.exit_code(), crate::util::exit::CONFIG);
assert!(
error.to_string().contains("export-key"),
"the refusal must say where a key is allowed to go: {error}"
);
}
}
}