iOSBuildServer/backend/automation/obfuscation/obfuscation_runtime/json_cache.py

129 lines
3.8 KiB
Python

import hashlib
import json
import os
from pathlib import Path
def load_json_file(path: str, default):
try:
return json.loads(Path(path).read_text(encoding="utf-8"))
except Exception:
return default() if callable(default) else default
def save_json_file(path: str, payload):
Path(path).write_text(
json.dumps(payload, ensure_ascii=False, indent=2),
encoding="utf-8",
)
class IncrementalAnalysisCache:
def __init__(self, cache_path: str, version: int):
self.cache_path = cache_path
self.version = version
self._state = None
self._dirty = False
self._memo = {}
def _default_state(self) -> dict:
return {
"version": self.version,
"files": {},
}
def load_state(self) -> dict:
if self._state is not None:
return self._state
payload = load_json_file(self.cache_path, self._default_state)
if payload.get("version") != self.version or not isinstance(payload.get("files"), dict):
payload = self._default_state()
self._state = payload
return self._state
def save_if_dirty(self):
if not self._dirty:
return
try:
save_json_file(self.cache_path, self.load_state())
self._dirty = False
except Exception:
pass
@staticmethod
def build_cache_key(analysis_kind: str, path: str) -> str:
return f"{analysis_kind}:{os.path.realpath(path)}"
@staticmethod
def _read_text_with_hash(path: str):
try:
text = Path(path).read_text(encoding="utf-8", errors="ignore")
except Exception:
return None, None
return text, hashlib.sha256(text.encode("utf-8")).hexdigest()
def get_or_compute(self, path: str, analysis_kind: str, analyzer):
cache_key = self.build_cache_key(analysis_kind, path)
memo_key = (analysis_kind, os.path.realpath(path))
if memo_key in self._memo:
return self._memo[memo_key]
cache_state = self.load_state()
files_cache = cache_state.setdefault("files", {})
cached_entry = files_cache.get(cache_key)
try:
stat = os.stat(path)
stat_signature = {
"mtime_ns": stat.st_mtime_ns,
"size": stat.st_size,
}
except Exception:
stat_signature = {
"mtime_ns": None,
"size": None,
}
if (
isinstance(cached_entry, dict)
and cached_entry.get("mtime_ns") == stat_signature["mtime_ns"]
and cached_entry.get("size") == stat_signature["size"]
and isinstance(cached_entry.get("analysis"), dict)
):
analysis = cached_entry["analysis"]
self._memo[memo_key] = analysis
return analysis
text, text_hash = self._read_text_with_hash(path)
if text is None:
analysis = analyzer("", path)
self._memo[memo_key] = analysis
return analysis
if (
isinstance(cached_entry, dict)
and cached_entry.get("sha256") == text_hash
and isinstance(cached_entry.get("analysis"), dict)
):
cached_entry.update(stat_signature)
self._dirty = True
analysis = cached_entry["analysis"]
self._memo[memo_key] = analysis
return analysis
analysis = analyzer(text, path)
files_cache[cache_key] = {
"path": os.path.realpath(path),
"analysis_kind": analysis_kind,
"mtime_ns": stat_signature["mtime_ns"],
"size": stat_signature["size"],
"sha256": text_hash,
"analysis": analysis,
}
self._dirty = True
self._memo[memo_key] = analysis
return analysis