use std::path::{Path, PathBuf};
const APPLICATION_DIRECTORY: &str = "forbidden-strings";
const ARTIFACT_FILENAME: &str = "rules.bin";
const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct SourceDigest(
[u8; 32],
);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct DigestError;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum CacheRootError {
InvalidOverride,
Unavailable,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum HostPlatform {
XdgUnix,
Macos,
Windows,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(super) struct CacheEnvironment {
pub(super) override_root: Option<std::ffi::OsString>,
pub(super) xdg_cache_home: Option<std::ffi::OsString>,
pub(super) home: Option<std::ffi::OsString>,
pub(super) local_app_data: Option<std::ffi::OsString>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct CacheLocation {
pub(super) application_directory: PathBuf,
pub(super) protected_directories: Vec<PathBuf>,
pub(super) artifact_path: PathBuf,
}
pub(super) fn source_digest(bytes: &[u8]) -> Result<SourceDigest, DigestError> {
let mut hasher = gix_hash::hasher(gix_hash::Kind::Sha256);
hasher.update(bytes);
let object_id = hasher.try_finalize().map_err(|_| return DigestError)?;
let digest: [u8; 32] = object_id.as_slice().try_into().map_err(|_| return DigestError)?;
return Ok(SourceDigest(digest))
}
#[cfg(not(test))]
pub(super) fn current_environment() -> CacheEnvironment {
return CacheEnvironment {
override_root: std::env::var_os("FORBIDDEN_STRINGS_CACHE_DIR"),
xdg_cache_home: std::env::var_os("XDG_CACHE_HOME"),
home: std::env::var_os("HOME"),
local_app_data: std::env::var_os("LOCALAPPDATA"),
}
}
#[cfg(test)]
pub(super) fn current_environment() -> CacheEnvironment {
return CacheEnvironment {
override_root: Some(
std::env::temp_dir()
.join(format!("forbidden-strings-unit-cache-{}", std::process::id()))
.into_os_string(),
),
..CacheEnvironment::default()
}
}
pub(super) fn current_platform() -> HostPlatform {
if cfg!(target_os = "macos") {
return HostPlatform::Macos;
}
if cfg!(target_os = "windows") {
return HostPlatform::Windows;
}
return HostPlatform::XdgUnix
}
pub(super) fn resolve_cache_root(
environment: &CacheEnvironment,
platform: HostPlatform,
) -> Result<PathBuf, CacheRootError> {
if let Some(override_root) = &environment.override_root {
return platform_absolute_path(override_root, platform)
.map_err(|_| return CacheRootError::InvalidOverride);
}
if platform == HostPlatform::Windows {
return environment_absolute_path(environment.local_app_data.as_ref(), platform);
}
if platform == HostPlatform::Macos {
return environment_absolute_path(environment.home.as_ref(), platform)
.map(|home| return home.join("Library").join("Caches"));
}
if let Some(xdg_cache_home) = &environment.xdg_cache_home {
let path = PathBuf::from(xdg_cache_home);
if path.is_absolute() {
return Ok(path);
}
}
return environment_absolute_path(environment.home.as_ref(), platform)
.map(|home| return home.join(".cache"))
}
fn environment_absolute_path(
value: Option<&std::ffi::OsString>,
platform: HostPlatform,
) -> Result<PathBuf, CacheRootError> {
let value = value.ok_or(CacheRootError::Unavailable)?;
return platform_absolute_path(value, platform)
}
fn platform_absolute_path(
value: &std::ffi::OsString,
platform: HostPlatform,
) -> Result<PathBuf, CacheRootError> {
let path = PathBuf::from(value);
if platform != HostPlatform::Windows {
if path.is_absolute() {
return Ok(path);
}
return Err(CacheRootError::Unavailable);
}
let rendered = value.to_string_lossy();
let bytes = rendered.as_bytes();
let has_drive_root = bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& (bytes[2] == b'\\' || bytes[2] == b'/');
let has_unc_root = rendered.starts_with("\\\\") || rendered.starts_with("//");
if has_drive_root || has_unc_root {
return Ok(path);
}
return Err(CacheRootError::Unavailable)
}
fn digest_hex(digest: SourceDigest) -> String {
let mut rendered = String::with_capacity(64);
for byte in digest.0 {
rendered.push(HEX_DIGITS[usize::from(byte >> 4)] as char);
rendered.push(HEX_DIGITS[usize::from(byte & 0x0f)] as char);
}
return rendered
}
pub(super) fn scanner_version() -> &'static str {
return env!("CARGO_PKG_VERSION")
}
pub(super) fn platform_identity() -> String {
return format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH)
}
fn compatibility_components() -> (String, String) {
return (format!("v{}", scanner_version()), platform_identity())
}
pub(super) fn cache_location(
root: &Path,
digest: SourceDigest,
) -> CacheLocation {
let application_directory = root.join(APPLICATION_DIRECTORY);
let (version, platform) = compatibility_components();
let version_directory = application_directory.join(version);
let platform_directory = version_directory.join(platform);
let digest_directory = platform_directory.join(digest_hex(digest));
let artifact_path = digest_directory.join(ARTIFACT_FILENAME);
return CacheLocation {
application_directory: application_directory.clone(),
protected_directories: vec![
application_directory,
version_directory,
platform_directory,
digest_directory,
],
artifact_path,
}
}
pub(super) fn digest_bytes(digest: SourceDigest) -> [u8; 32] {
return digest.0
}
impl std::fmt::Display for CacheRootError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self == &CacheRootError::InvalidOverride {
return formatter.write_str("FORBIDDEN_STRINGS_CACHE_DIR must be an absolute path");
}
return formatter.write_str("per-user cache root is unavailable")
}
}
impl std::fmt::Display for DigestError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str("SHA-256 source fingerprint failed")
}
}
impl std::error::Error for CacheRootError {}
impl std::error::Error for DigestError {}
#[cfg(test)]
#[path = "path_tests.rs"]
mod tests;