#[cfg_attr(not(test), expect(clippy::wildcard_imports, reason = "shared"))]
use super::*;
#[doc = " Direct cache access for readers that cannot express themselves as a single"]
#[doc = " `gh` invocation: a response judged by its body rather than its exit status,"]
#[doc = " or bytes produced by Git rather than by GitHub."]
pub(crate) fn cache_read(key: &str, life: CacheLife) -> Option<Vec<u8>> {
cache_read_bounded(key, life, MAX_GH_METADATA_BYTES)
}
pub(crate) fn cache_read_bounded(key: &str, life: CacheLife, limit: usize) -> Option<Vec<u8>> {
CacheStore::discover()?
.read(key, limit)
.filter(|entry| life.accepts(entry.age))
.map(|entry| entry.data)
}
pub(crate) fn cache_write(key: &str, data: &[u8]) {
cache_write_bounded(key, data, MAX_GH_METADATA_BYTES);
}
pub(crate) fn cache_write_bounded(key: &str, data: &[u8], limit: usize) {
if let Some(cache) = CacheStore::discover() {
drop(cache.write(key, data, limit));
}
}
#[cfg(not(test))]
pub(crate) fn recent_pull_requests() -> Vec<RecentPullRequest> {
let mut recent = cache_read(RECENT_PULL_REQUESTS_CACHE_KEY, CacheLife::Immutable)
.and_then(|data| serde_json::from_slice::<Vec<RecentPullRequest>>(&data).ok())
.unwrap_or_default();
if let Some(cache) = CacheStore::discover() {
for pull_request in cache.cached_pull_requests() {
if recent.iter().any(|existing| {
existing.number == pull_request.number
&& existing
.repository
.url
.eq_ignore_ascii_case(&pull_request.repository.url)
}) {
continue;
}
recent.push(pull_request);
if recent.len() == MAX_RECENT_PULL_REQUESTS {
break;
}
}
}
recent.truncate(MAX_RECENT_PULL_REQUESTS);
recent
}
#[cfg(not(test))]
pub(crate) fn record_recent_pull_request(pull_request: &PullRequest) -> Vec<RecentPullRequest> {
let current = RecentPullRequest::from(pull_request);
let mut recent = recent_pull_requests();
recent.retain(|existing| {
existing.number != current.number
|| !existing
.repository
.url
.eq_ignore_ascii_case(¤t.repository.url)
});
recent.insert(0, current);
recent.truncate(MAX_RECENT_PULL_REQUESTS);
if let Ok(data) = serde_json::to_vec(&recent) {
cache_write(RECENT_PULL_REQUESTS_CACHE_KEY, &data);
}
recent
}
pub(super) struct CacheEntry {
pub(super) data: Vec<u8>,
pub(super) age: Duration,
}
pub(super) struct CacheStore {
root: PathBuf,
}
impl CacheStore {
pub(super) fn discover() -> Option<Self> {
cache_root().map(|root| Self {
root: root.join("github"),
})
}
#[cfg(test)]
pub(super) const fn at(root: PathBuf) -> Self {
Self { root }
}
pub(super) fn read(&self, key: &str, limit: usize) -> Option<CacheEntry> {
let path = self.path(key);
let metadata = fs::metadata(&path).ok()?;
if metadata.len() > (limit as u64).saturating_add(CACHE_MAGIC.len() as u64) {
drop(fs::remove_file(path));
return None;
}
let mut data = fs::read(path).ok()?;
if !data.starts_with(CACHE_MAGIC) {
return None;
}
drop(data.drain(..CACHE_MAGIC.len()));
let age = metadata
.modified()
.ok()
.and_then(|modified| SystemTime::now().duration_since(modified).ok())
.unwrap_or_default();
Some(CacheEntry { data, age })
}
pub(super) fn write(&self, key: &str, data: &[u8], limit: usize) -> Result<()> {
if data.len() > limit {
return Ok(());
}
create_private_directory(&self.root)?;
let destination = self.path(key);
let id = CACHE_WRITE_ID.fetch_add(1, Ordering::Relaxed);
let temporary = self
.root
.join(format!(".write-{}-{id}.tmp", std::process::id()));
let mut options = OpenOptions::new();
let _ = options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let _ = options.mode(0o600);
}
let mut file = options.open(&temporary)?;
file.write_all(CACHE_MAGIC)?;
file.write_all(data)?;
file.flush()?;
drop(file);
if destination.exists() {
drop(fs::remove_file(&destination));
}
if let Err(error) = fs::rename(&temporary, &destination) {
drop(fs::remove_file(&temporary));
return Err(error.into());
}
self.prune();
Ok(())
}
pub(super) fn path(&self, key: &str) -> PathBuf {
let (left, right) = stable_cache_hash(key.as_bytes());
self.root.join(format!("{left:016x}{right:016x}.cache"))
}
pub(super) fn cached_pull_requests(&self) -> Vec<RecentPullRequest> {
let Ok(entries) = fs::read_dir(&self.root) else {
return Vec::new();
};
let mut files = entries
.filter_map(Result::ok)
.filter_map(|entry| {
let path = entry.path();
if path.extension().and_then(OsStr::to_str) != Some("cache") {
return None;
}
let metadata = entry.metadata().ok()?;
if metadata.len() > MAX_RECENT_CACHE_ENTRY_BYTES {
return None;
}
Some((metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH), path))
})
.collect::<Vec<_>>();
files.sort_by_key(|(modified, _)| std::cmp::Reverse(*modified));
files
.into_iter()
.take(MAX_RECENT_CACHE_SCAN)
.filter_map(|(_, path)| cached_pull_request_at(&path))
.take(MAX_RECENT_PULL_REQUESTS)
.collect()
}
fn prune(&self) {
let Ok(entries) = fs::read_dir(&self.root) else {
return;
};
let mut files = entries
.filter_map(Result::ok)
.filter_map(|entry| {
let path = entry.path();
if path.extension().and_then(OsStr::to_str) != Some("cache") {
return None;
}
let metadata = entry.metadata().ok()?;
Some((
metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH),
metadata.len(),
path,
))
})
.collect::<Vec<_>>();
files.sort_by_key(|(modified, ..)| *modified);
let mut total = files.iter().map(|(_, bytes, _)| *bytes).sum::<u64>();
let mut count = files.len();
for (_, bytes, path) in files {
if count <= MAX_CACHE_ENTRIES && total <= MAX_CACHE_BYTES {
break;
}
if fs::remove_file(path).is_ok() {
count = count.saturating_sub(1);
total = total.saturating_sub(bytes);
}
}
}
}
pub(super) fn cached_pull_request_at(path: &Path) -> Option<RecentPullRequest> {
let data = fs::read(path).ok()?;
let body = data.strip_prefix(CACHE_MAGIC)?;
let record = body
.split(|byte| *byte == b'\n')
.find(|record| !record.is_empty())?;
let fields = parse_tsv_record::<PULL_REQUEST_TSV_FIELDS>(record).ok()?;
let number = fields.get(1)?.parse::<u64>().ok()?;
let url = fields.get(8)?;
let repository = repository_from_pull_request_url(url, number)?;
Some(RecentPullRequest {
number,
title: bounded_text(fields.get(2)?, MAX_PULL_REQUEST_TITLE_BYTES),
updated_at: fields.get(7)?.to_owned(),
repository,
})
}
pub(super) fn repository_from_pull_request_url(url: &str, number: u64) -> Option<GitHubRepository> {
let suffix = format!("/pull/{number}");
let repository_url = url.strip_suffix(&suffix)?.trim_end_matches('/');
let (_, rest) = repository_url.split_once("://")?;
let (_, path) = rest.split_once('/')?;
let mut components = path.trim_matches('/').split('/');
let owner = components.next()?;
let name = components.next()?;
if owner.is_empty() || name.is_empty() || components.next().is_some() {
return None;
}
Some(GitHubRepository {
name_with_owner: format!("{owner}/{name}"),
url: repository_url.to_owned(),
remotes: Vec::new(),
})
}
pub(super) fn cache_root() -> Option<PathBuf> {
if let Some(path) = env::var_os("QUINJET_CACHE_DIR").filter(|path| !path.is_empty()) {
return Some(PathBuf::from(path));
}
#[cfg(windows)]
{
if let Some(path) = env::var_os("LOCALAPPDATA").filter(|path| !path.is_empty()) {
return Some(PathBuf::from(path).join("quinjet").join("cache"));
}
}
if let Some(path) = env::var_os("XDG_CACHE_HOME").filter(|path| !path.is_empty()) {
return Some(PathBuf::from(path).join("quinjet"));
}
#[cfg(target_os = "macos")]
{
if let Some(path) = env::var_os("HOME").filter(|path| !path.is_empty()) {
return Some(
PathBuf::from(path)
.join("Library")
.join("Caches")
.join("quinjet"),
);
}
}
env::var_os("HOME")
.filter(|path| !path.is_empty())
.map(|path| PathBuf::from(path).join(".cache").join("quinjet"))
}
pub(super) fn create_private_directory(path: &Path) -> Result<()> {
fs::create_dir_all(path)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o700))?;
}
Ok(())
}
pub(super) fn stable_cache_hash(value: &[u8]) -> (u64, u64) {
let mut left = 0xcbf2_9ce4_8422_2325_u64;
let mut right = 0x8422_2325_cbf2_9ce4_u64;
for byte in value {
left ^= u64::from(*byte);
left = left.wrapping_mul(0x0100_0000_01b3);
right ^= u64::from(*byte).rotate_left(1);
right = right.wrapping_mul(0x0100_0000_01b3).rotate_left(5);
}
(left, right)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cache_limits_accept_exact_data_and_reject_oversized_data() {
let (directory, cache) = cache_store();
cache.write("empty", b"", 0).unwrap();
cache.write("exact", b"data", 4).unwrap();
cache.write("write-over", b"large", 4).unwrap();
assert_eq!(cache.read("empty", 0).unwrap().data, b"");
assert_eq!(cache.read("exact", 4).unwrap().data, b"data");
assert!(!cache.path("write-over").exists());
let mut oversized = CACHE_MAGIC.to_vec();
oversized.extend_from_slice(b"large");
write_raw(&cache, "read-over", &oversized);
assert!(cache.read("read-over", 4).is_none());
assert!(!cache.path("read-over").exists());
assert!(directory.path().exists());
}
#[test]
fn invalid_and_truncated_magic_are_rejected() {
let (_directory, cache) = cache_store();
let cases = [
("empty-file", Vec::new()),
("truncated", CACHE_MAGIC[..CACHE_MAGIC.len() - 1].to_vec()),
("wrong", b"quinjet-gh-cache-v2\nbody".to_vec()),
];
for (key, data) in cases {
write_raw(&cache, key, &data);
assert!(cache.read(key, usize::MAX).is_none(), "{key}");
}
write_raw(&cache, "magic-only", CACHE_MAGIC);
assert_eq!(cache.read("magic-only", 0).unwrap().data, b"");
}
#[test]
fn overwrite_replaces_data_and_failed_rename_cleans_temporary_file() {
let (_directory, cache) = cache_store();
cache.write("same", b"old", 16).unwrap();
cache.write("same", b"new", 16).unwrap();
assert_eq!(cache.read("same", 16).unwrap().data, b"new");
assert_eq!(temporary_paths(&cache.root), Vec::<PathBuf>::new());
fs::create_dir_all(cache.path("blocked")).unwrap();
assert!(cache.write("blocked", b"data", 16).is_err());
assert_eq!(temporary_paths(&cache.root), Vec::<PathBuf>::new());
}
#[test]
fn stable_hashes_and_paths_are_fixed() {
assert_eq!(
stable_cache_hash(b""),
(0xcbf2_9ce4_8422_2325, 0x8422_2325_cbf2_9ce4)
);
assert_eq!(
stable_cache_hash(b"repo\npage 1"),
(0x3001_1e10_6f1a_b939, 0xb1ee_1cac_8bc6_e6c9)
);
let (_directory, cache) = cache_store();
assert_eq!(
cache
.path("repo\npage 1")
.file_name()
.and_then(OsStr::to_str),
Some("30011e106f1ab939b1ee1cac8bc6e6c9.cache")
);
assert_ne!(cache.path("same"), cache.path("different"));
}
#[test]
fn pull_request_urls_require_an_exact_repository_and_number() {
let repository =
repository_from_pull_request_url("https://github.example.com/Acme/Widget/pull/42", 42)
.unwrap();
assert_eq!(repository.name_with_owner, "Acme/Widget");
assert_eq!(repository.url, "https://github.example.com/Acme/Widget");
assert_eq!(repository.remotes, Vec::<String>::new());
for url in [
"github.com/acme/widget/pull/42",
"https://github.com/acme/pull/42",
"https://github.com/acme/widget/extra/pull/42",
"https://github.com/acme/widget/pull/41",
"https://github.com/acme/widget/pull/42/files",
"https://github.com/acme/widget/pull/42?diff=split",
"https://github.com//widget/pull/42",
] {
assert!(repository_from_pull_request_url(url, 42).is_none(), "{url}");
}
}
#[cfg(unix)]
#[test]
fn cache_directories_and_files_are_private() {
use std::os::unix::fs::PermissionsExt;
let (_directory, cache) = cache_store();
cache.write("private", b"data", 4).unwrap();
let directory_mode = fs::metadata(&cache.root).unwrap().permissions().mode();
let file_mode = fs::metadata(cache.path("private"))
.unwrap()
.permissions()
.mode();
assert_eq!(directory_mode & 0o777, 0o700);
assert_eq!(file_mode & 0o077, 0);
}
fn cache_store() -> (tempfile::TempDir, CacheStore) {
let directory = tempfile::tempdir().unwrap();
let cache = CacheStore::at(directory.path().join("github"));
(directory, cache)
}
fn write_raw(cache: &CacheStore, key: &str, data: &[u8]) {
fs::create_dir_all(&cache.root).unwrap();
fs::write(cache.path(key), data).unwrap();
}
fn temporary_paths(root: &Path) -> Vec<PathBuf> {
fs::read_dir(root)
.unwrap()
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(OsStr::to_str)
.is_some_and(|name| name.starts_with(".write-"))
})
.collect()
}
}