use crate::diagnostic_log::debug;
use crate::child_environment::environment_value;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
pub const VERSION_KIND: &str = "git-version";
fn file_tag(kind: &str) -> String {
return format!("cli-git {kind} cache 1\n");
}
const TEXT_LIMIT: usize = 1024;
const TAG: &str = "git-version-cache";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BinaryIdentity {
pub path: String,
pub text: String,
}
#[cfg(unix)]
pub fn binary_identity(real_git: &Path) -> Option<BinaryIdentity> {
use std::io::Read;
use std::os::unix::fs::MetadataExt;
let canonical: PathBuf = std::fs::canonicalize(real_git).ok()?;
if canonical.file_name()?.to_str()? != "git" {
return None;
}
let path: &str = canonical.to_str()?;
if path.contains('\n') || path.contains('\t') {
return None;
}
let mut file: std::fs::File = std::fs::File::open(&canonical).ok()?;
let metadata: std::fs::Metadata = file.metadata().ok()?;
if !metadata.is_file() {
return None;
}
let mut magic: [u8; 4] = [0; 4];
file.read_exact(&mut magic).ok()?;
if magic != *b"\x7fELF" {
return None;
}
let text: String = format!(
"{path}\t{}\t{}.{}\t{}.{}\t{}\t{}",
metadata.len(),
metadata.mtime(),
metadata.mtime_nsec(),
metadata.ctime(),
metadata.ctime_nsec(),
metadata.dev(),
metadata.ino()
);
return Some(BinaryIdentity { path: String::from(path), text });
}
#[cfg(not(unix))]
pub fn binary_identity(real_git: &Path) -> Option<BinaryIdentity> {
let _ = real_git;
return None;
}
pub fn cache_directory(environment: &[(OsString, OsString)]) -> Option<PathBuf> {
if let Some(cache_home) = environment_value(environment, "XDG_CACHE_HOME") {
let cache_path: PathBuf = PathBuf::from(cache_home);
if cache_path.is_absolute() {
return Some(cache_path.join("cli-git"));
}
}
let home: OsString = environment_value(environment, "HOME")?;
if home.is_empty() {
return None;
}
return Some(PathBuf::from(home).join(".cache").join("cli-git"));
}
fn cache_file(directory: &Path, identity: &BinaryIdentity, kind: &str) -> PathBuf {
use std::hash::{Hash, Hasher};
let mut hasher: std::collections::hash_map::DefaultHasher = std::collections::hash_map::DefaultHasher::new();
identity.path.hash(&mut hasher);
return directory.join(format!("{kind}-{:016x}", hasher.finish()));
}
#[cfg(unix)]
fn is_private_file_of_this_user(path: &Path) -> bool {
use std::os::unix::fs::MetadataExt;
let Ok(metadata) = std::fs::symlink_metadata(path) else {
return false;
};
let owner: u32 = unsafe { libc::geteuid() };
return metadata.is_file() && metadata.uid() == owner && metadata.mode() & 0o077 == 0;
}
#[cfg(not(unix))]
fn is_private_file_of_this_user(path: &Path) -> bool {
let _ = path;
return false;
}
pub fn read_version_text(directory: &Path, identity: &BinaryIdentity) -> Option<Vec<u8>> {
return read_cached_text(directory, identity, VERSION_KIND);
}
pub fn read_cached_text(directory: &Path, identity: &BinaryIdentity, kind: &str) -> Option<Vec<u8>> {
let path: PathBuf = cache_file(directory, identity, kind);
if !is_private_file_of_this_user(path.as_path()) {
return None;
}
let bytes: Vec<u8> = crate::private_storage::read_regular_file(path.as_path(), 4096).ok()?;
let rest: &[u8] = bytes.strip_prefix(file_tag(kind).as_bytes())?;
let rest_after_identity: &[u8] = rest.strip_prefix(identity.text.as_bytes())?;
let text: &[u8] = rest_after_identity.strip_prefix(b"\n")?;
return Some(text.to_vec());
}
pub fn store_version_text(directory: &Path, identity: &BinaryIdentity, text: &[u8]) {
store_cached_text(directory, identity, VERSION_KIND, text);
}
pub fn store_cached_text(directory: &Path, identity: &BinaryIdentity, kind: &str, text: &[u8]) {
if text.len() > TEXT_LIMIT {
return;
}
if let Some(parent) = directory.parent()
&& let Err(error) = std::fs::create_dir_all(parent)
{
debug(TAG, format!("could not create {}: {error}", parent.display()).as_str());
return;
}
if let Err(error) = crate::private_storage::create_private_directory(directory)
&& error.kind() != std::io::ErrorKind::AlreadyExists
{
debug(TAG, format!("could not create {}: {error}", directory.display()).as_str());
return;
}
let unique: String = match crate::random_id::random_uuid() {
Ok(id) => id,
Err(error) => {
debug(TAG, format!("no random name for the cache file: {}", error.0).as_str());
return;
}
};
let temporary: PathBuf = directory.join(format!(".tmp-{unique}"));
let tag: String = file_tag(kind);
let mut bytes: Vec<u8> = Vec::with_capacity(tag.len() + identity.text.len() + 1 + text.len());
bytes.extend_from_slice(tag.as_bytes());
bytes.extend_from_slice(identity.text.as_bytes());
bytes.push(b'\n');
bytes.extend_from_slice(text);
if let Err(error) = crate::private_storage::write_exclusive_scratch(temporary.as_path(), bytes.as_slice()) {
debug(TAG, format!("could not write {}: {error}", temporary.display()).as_str());
return;
}
if let Err(error) = std::fs::rename(&temporary, cache_file(directory, identity, kind)) {
debug(TAG, format!("could not publish the cache file: {error}").as_str());
if let Err(removal) = std::fs::remove_file(&temporary) {
debug(TAG, format!("could not remove {}: {removal}", temporary.display()).as_str());
}
}
}
#[cfg(test)]
#[path = "git_version_cache_tests.rs"]
mod tests;