use std::collections::hash_map::DefaultHasher; use std::fs; use std::fs::{create_dir_all, File, Metadata}; use std::hash::{Hash, Hasher}; use std::io::Write; use std::path::Path; use anyhow::Result; use filetime::FileTime; use log::error; use path_absolutize::Absolutize; use serde::{Deserialize, Serialize}; use crate::autofix::fixer; use crate::message::Message; use crate::settings::Settings; const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); #[derive(Serialize, Deserialize)] struct CacheMetadata { mtime: i64, } #[derive(Serialize)] struct CheckResultRef<'a> { metadata: &'a CacheMetadata, messages: &'a [Message], } #[derive(Deserialize)] struct CheckResult { metadata: CacheMetadata, messages: Vec, } pub enum Mode { ReadWrite, ReadOnly, WriteOnly, None, } impl Mode { fn allow_read(&self) -> bool { match self { Mode::ReadWrite => true, Mode::ReadOnly => true, Mode::WriteOnly => false, Mode::None => false, } } fn allow_write(&self) -> bool { match self { Mode::ReadWrite => true, Mode::ReadOnly => false, Mode::WriteOnly => true, Mode::None => false, } } } impl From for Mode { fn from(value: bool) -> Self { if value { Mode::ReadWrite } else { Mode::None } } } fn cache_dir() -> &'static str { "./.ruff_cache" } fn content_dir() -> &'static str { "content" } fn cache_key(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> u64 { let mut hasher = DefaultHasher::new(); CARGO_PKG_VERSION.hash(&mut hasher); path.absolutize().unwrap().hash(&mut hasher); settings.hash(&mut hasher); autofix.hash(&mut hasher); hasher.finish() } /// Initialize the cache directory. pub fn init() -> Result<()> { let path = Path::new(cache_dir()); // Create the cache directories. create_dir_all(path.join(content_dir()))?; // Add the CACHEDIR.TAG. if !cachedir::is_tagged(path)? { cachedir::add_tag(path)?; } // Add the .gitignore. let gitignore_path = path.join(".gitignore"); if !gitignore_path.exists() { let mut file = File::create(gitignore_path)?; file.write_all(b"*")?; } Ok(()) } fn write_sync(key: u64, value: &[u8]) -> Result<(), std::io::Error> { fs::write( Path::new(cache_dir()) .join(content_dir()) .join(format!("{key:x}")), value, ) } fn read_sync(key: u64) -> Result, std::io::Error> { fs::read( Path::new(cache_dir()) .join(content_dir()) .join(format!("{key:x}")), ) } /// Get a value from the cache. pub fn get( path: &Path, metadata: &Metadata, settings: &Settings, autofix: &fixer::Mode, mode: &Mode, ) -> Option> { if !mode.allow_read() { return None; }; let encoded = read_sync(cache_key(path, settings, autofix)).ok()?; let (mtime, messages) = match bincode::deserialize::(&encoded[..]) { Ok(CheckResult { metadata: CacheMetadata { mtime }, messages, }) => (mtime, messages), Err(e) => { error!("Failed to deserialize encoded cache entry: {e:?}"); return None; } }; if FileTime::from_last_modification_time(metadata).unix_seconds() != mtime { return None; } Some(messages) } /// Set a value in the cache. pub fn set( path: &Path, metadata: &Metadata, settings: &Settings, autofix: &fixer::Mode, messages: &[Message], mode: &Mode, ) { if !mode.allow_write() { return; }; let check_result = CheckResultRef { metadata: &CacheMetadata { mtime: FileTime::from_last_modification_time(metadata).unix_seconds(), }, messages, }; if let Err(e) = write_sync( cache_key(path, settings, autofix), &bincode::serialize(&check_result).unwrap(), ) { error!("Failed to write to cache: {e:?}"); } }