use crate::paths::EnsureParentDirExists;
use eyre::ContextCompat;
use facet::Facet;
use std::fs;
use std::io;
use std::ops::Deref;
use std::path::Path;
use std::path::PathBuf;
use tracing::debug;
use tracing::instrument;
pub const MACHINE_ROOT_DIR_NAME: &str = "teamy_mft";
pub const MACHINE_CONFIG_FILE_NAME: &str = "machine_config.json";
pub const DEFAULT_SERVICE_NAME: &str = "teamy-mft-daemon";
pub const DEFAULT_PIPE_NAME: &str = r"\\.\pipe\teamy-mft-daemon";
pub const DEFAULT_IDLE_TIMEOUT_SECS: u64 = 300;
pub const MFT_CACHE_FILE_EXTENSION: &str = ".mft";
pub const SEARCH_INDEX_FILE_EXTENSION: &str = ".mft_search_index";
pub const SEARCH_INDEX_TEMP_FILE_EXTENSION: &str = "mft_search_index.tmp";
pub const OVERLAY_SEARCH_INDEX_FILE_EXTENSION: &str = ".mft_overlay_search_index";
pub const OVERLAY_SEARCH_INDEX_TEMP_FILE_EXTENSION: &str = "mft_overlay_search_index.tmp";
pub const CHECKPOINT_FILE_EXTENSION: &str = ".mft_checkpoint.json";
pub const DEFAULT_IGNORED_RULES_PATH_PATTERNS: &[&str] = &[
"**/$RECYCLE.BIN/**",
"**/AppData/Roaming/Code/User/History/**",
"**/AppData/Roaming/Cursor/User/History/**",
"**/AppData/Roaming/Windsurf/User/History/**",
"**/.config/Code/User/History/**",
"**/.config/Cursor/User/History/**",
"**/.config/Windsurf/User/History/**",
"**/.vscode/**/History/**",
"**/CARGO_HOME/registry/src/**",
"**/.cargo/registry/src/**",
"**/cargo/registry/src/**",
];
#[derive(Debug, Clone, PartialEq, Eq, Facet)]
pub struct MachineConfig {
pub version: u32,
pub owner_sid: String,
pub sync_dir: FacetPathBuf,
pub pipe_name: String,
pub service_name: String,
pub idle_timeout_secs: u64,
#[facet(default)]
pub ignored_rules_path_patterns: Vec<String>,
}
impl MachineConfig {
#[must_use]
pub fn new(owner_sid: String, sync_dir: Option<PathBuf>) -> Self {
Self {
version: 1,
owner_sid,
sync_dir: sync_dir.unwrap_or_else(default_sync_dir).into(),
pipe_name: String::from(DEFAULT_PIPE_NAME),
service_name: String::from(DEFAULT_SERVICE_NAME),
idle_timeout_secs: DEFAULT_IDLE_TIMEOUT_SECS,
ignored_rules_path_patterns: default_ignored_rules_path_patterns(),
}
}
#[must_use]
pub fn with_normalized_defaults(mut self) -> Self {
if self.ignored_rules_path_patterns.is_empty() {
self.ignored_rules_path_patterns = default_ignored_rules_path_patterns();
}
self
}
}
#[must_use]
pub fn default_ignored_rules_path_patterns() -> Vec<String> {
DEFAULT_IGNORED_RULES_PATH_PATTERNS
.iter()
.map(|pattern| (*pattern).to_owned())
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(transparent)]
pub struct FacetPathBuf(PathBuf);
impl FacetPathBuf {
#[must_use]
pub fn into_inner(self) -> PathBuf {
self.0
}
}
impl Deref for FacetPathBuf {
type Target = PathBuf;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<PathBuf> for FacetPathBuf {
fn from(value: PathBuf) -> Self {
Self(value)
}
}
impl From<FacetPathBuf> for PathBuf {
fn from(value: FacetPathBuf) -> Self {
value.0
}
}
impl AsRef<Path> for FacetPathBuf {
fn as_ref(&self) -> &Path {
&self.0
}
}
impl std::fmt::Display for FacetPathBuf {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.display().fmt(f)
}
}
unsafe fn facet_path_buf_proxy_convert_out(
target_ptr: facet::PtrConst,
proxy_ptr: facet::PtrUninit,
) -> Result<facet::PtrMut, String> {
unsafe {
let path = target_ptr.get::<FacetPathBuf>();
let display_string = path.0.display().to_string();
let roundtrip = PathBuf::from(&display_string);
if roundtrip != path.0 {
return Err(format!(
"Path {} cannot be safely serialized through display() without loss",
path.0.display()
));
}
#[allow(
clippy::cast_ptr_alignment,
reason = "facet allocates proxy storage with the alignment required by the proxy type"
)]
let proxy_mut = proxy_ptr.as_mut_byte_ptr().cast::<String>();
proxy_mut.write(display_string);
Ok(facet::PtrMut::new(proxy_mut.cast::<u8>()))
}
}
unsafe fn facet_path_buf_proxy_convert_in(
proxy_ptr: facet::PtrConst,
target_ptr: facet::PtrUninit,
) -> Result<facet::PtrMut, String> {
unsafe {
let display_string = proxy_ptr.read::<String>();
let roundtrip = PathBuf::from(&display_string);
let redisplay = roundtrip.display().to_string();
if redisplay != display_string {
return Err(format!(
"Path {display_string} did not round-trip cleanly through display()"
));
}
#[allow(
clippy::cast_ptr_alignment,
reason = "facet allocates target storage with the alignment required by the target type"
)]
let target_mut = target_ptr.as_mut_byte_ptr().cast::<FacetPathBuf>();
target_mut.write(FacetPathBuf(roundtrip));
Ok(facet::PtrMut::new(target_mut.cast::<u8>()))
}
}
const FACET_PATH_BUF_PROXY: facet::ProxyDef = facet::ProxyDef {
shape: <String as Facet>::SHAPE,
convert_in: facet_path_buf_proxy_convert_in,
convert_out: facet_path_buf_proxy_convert_out,
};
unsafe impl Facet<'_> for FacetPathBuf {
const SHAPE: &'static facet::Shape = &const {
facet::ShapeBuilder::for_sized::<FacetPathBuf>("FacetPathBuf")
.module_path("teamy_mft::machine::config")
.ty(facet::Type::User(facet::UserType::Opaque))
.def(facet::Def::Scalar)
.proxy(&FACET_PATH_BUF_PROXY)
.build()
};
}
#[derive(Debug, Clone, PartialEq, Eq, Facet)]
pub struct PublishedCheckpoint {
pub drive_letter: char,
pub volume_serial_number: Option<u32>,
pub journal_id: Option<u64>,
pub snapshot_usn: Option<u64>,
pub last_usn: Option<u64>,
pub published_at_unix_ms: u64,
pub overlay_row_count: u64,
pub base_index_version: u16,
}
impl PublishedCheckpoint {
#[must_use]
pub fn empty(drive_letter: char, base_index_version: u16) -> Self {
Self {
drive_letter,
volume_serial_number: None,
journal_id: None,
snapshot_usn: None,
last_usn: None,
published_at_unix_ms: current_unix_ms(),
overlay_row_count: 0,
base_index_version,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PublishedDrivePaths {
pub drive_letter: char,
pub mft_path: PathBuf,
pub base_index_path: PathBuf,
pub overlay_index_path: PathBuf,
pub checkpoint_path: PathBuf,
}
#[must_use]
pub fn program_data_dir() -> PathBuf {
std::env::var_os("PROGRAMDATA").map_or_else(|| PathBuf::from(r"C:\ProgramData"), PathBuf::from)
}
#[must_use]
pub fn machine_root_dir() -> PathBuf {
program_data_dir().join(MACHINE_ROOT_DIR_NAME)
}
#[must_use]
pub fn machine_config_path() -> PathBuf {
machine_root_dir().join(MACHINE_CONFIG_FILE_NAME)
}
#[must_use]
pub fn default_sync_dir() -> PathBuf {
machine_root_dir().join("cache")
}
#[must_use]
pub fn published_drive_paths(sync_dir: &Path, drive_letter: char) -> PublishedDrivePaths {
PublishedDrivePaths {
drive_letter,
mft_path: sync_dir.join(format!("{drive_letter}{MFT_CACHE_FILE_EXTENSION}")),
base_index_path: sync_dir.join(format!("{drive_letter}{SEARCH_INDEX_FILE_EXTENSION}")),
overlay_index_path: sync_dir.join(format!(
"{drive_letter}{OVERLAY_SEARCH_INDEX_FILE_EXTENSION}"
)),
checkpoint_path: sync_dir.join(format!("{drive_letter}{CHECKPOINT_FILE_EXTENSION}")),
}
}
pub fn load_machine_config() -> eyre::Result<Option<MachineConfig>> {
let path = machine_config_path();
match fs::metadata(&path) {
Ok(metadata) if metadata.is_file() => {}
Ok(metadata) => {
eyre::bail!(
"Machine config path {} exists but is not a file; file_type={:?}",
path.display(),
metadata.file_type()
);
}
Err(error) if error.kind() == io::ErrorKind::NotFound => {
debug!(path = %path.display(), "Machine config file is not present");
return Ok(None);
}
Err(error) => {
return Err(eyre::eyre!(
"Failed reading machine config metadata at {}: {error}",
path.display()
));
}
}
let config = facet_json::from_str::<MachineConfig>(&fs::read_to_string(&path)?)
.map_err(|error| eyre::eyre!("Failed parsing {}: {error}", path.display()))?;
Ok(Some(config.with_normalized_defaults()))
}
pub fn save_machine_config(config: &MachineConfig) -> eyre::Result<()> {
let path = machine_config_path();
path.ensure_parent_dir_exists()?;
let parent = path
.parent()
.wrap_err_with(|| format!("Machine config path {} has no parent", path.display()))?;
let test_path = parent.join("machine_config.write_test.tmp");
let bytes = facet_json::to_vec_pretty(config)?;
debug!(
path = %path.display(),
parent = %parent.display(),
test_path = %test_path.display(),
"Saving machine config"
);
fs::write(&test_path, b"ok").map_err(|error| {
eyre::eyre!(
"Failed creating machine config probe file at {} before writing {}: {error}",
test_path.display(),
path.display()
)
})?;
let _ = fs::remove_file(&test_path);
if path.exists() {
debug!(
path = %path.display(),
"Machine config already exists; repairing permissions before overwrite"
);
crate::machine::security::restrict_path_to_owner(&path, &config.owner_sid)?;
fs::remove_file(&path).map_err(|error| {
eyre::eyre!(
"Failed removing stale machine config at {} before overwrite: {error}",
path.display()
)
})?;
}
fs::write(&path, &bytes).map_err(|error| {
eyre::eyre!(
"Failed writing machine config at {} after successful probe in {}: {error}",
path.display(),
parent.display()
)
})?;
Ok(())
}
#[instrument(level = "debug")]
#[track_caller]
pub fn load_required_machine_config() -> eyre::Result<MachineConfig> {
load_machine_config()?.wrap_err("Teamy-MFT is not installed. Run `teamy-mft install --help`.")
}
pub fn load_machine_client_config() -> eyre::Result<MachineConfig> {
match load_machine_config() {
Ok(Some(config)) => Ok(config),
Ok(None) => Ok(MachineConfig {
version: 1,
owner_sid: String::new(),
sync_dir: default_sync_dir().into(),
pipe_name: String::from(DEFAULT_PIPE_NAME),
service_name: String::from(DEFAULT_SERVICE_NAME),
idle_timeout_secs: DEFAULT_IDLE_TIMEOUT_SECS,
ignored_rules_path_patterns: default_ignored_rules_path_patterns(),
}),
Err(error) if is_access_denied_error(&error) => Ok(MachineConfig {
version: 1,
owner_sid: String::new(),
sync_dir: default_sync_dir().into(),
pipe_name: String::from(DEFAULT_PIPE_NAME),
service_name: String::from(DEFAULT_SERVICE_NAME),
idle_timeout_secs: DEFAULT_IDLE_TIMEOUT_SECS,
ignored_rules_path_patterns: default_ignored_rules_path_patterns(),
}),
Err(error) => Err(error),
}
}
#[instrument(level = "debug")]
#[track_caller]
pub fn load_sync_dir_from_config() -> eyre::Result<PathBuf> {
let config = load_required_machine_config()?;
debug!(sync_dir = %config.sync_dir.display(), "Resolved machine sync directory");
Ok(config.sync_dir.into_inner())
}
pub fn load_checkpoint(path: &Path) -> eyre::Result<Option<PublishedCheckpoint>> {
if !path.is_file() {
return Ok(None);
}
let checkpoint = facet_json::from_str::<PublishedCheckpoint>(&fs::read_to_string(path)?)
.map_err(|error| eyre::eyre!("Failed parsing {}: {error}", path.display()))?;
Ok(Some(checkpoint))
}
pub fn save_checkpoint(path: &Path, checkpoint: &PublishedCheckpoint) -> eyre::Result<()> {
path.ensure_parent_dir_exists()?;
fs::write(path, facet_json::to_vec_pretty(checkpoint)?)?;
Ok(())
}
#[must_use]
pub fn current_unix_ms() -> u64 {
#[allow(
clippy::cast_possible_truncation,
reason = "Unix milliseconds fit in u64 for practical system lifetimes"
)]
{
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
}
#[must_use]
pub fn is_access_denied_error(error: &eyre::Report) -> bool {
error
.chain()
.filter_map(|source| source.downcast_ref::<io::Error>())
.any(|source| source.kind() == io::ErrorKind::PermissionDenied)
}
#[cfg(test)]
mod tests {
use super::DEFAULT_IGNORED_RULES_PATH_PATTERNS;
use super::MachineConfig;
#[test]
fn new_machine_config_includes_default_ignored_rules_paths() {
let config = MachineConfig::new(String::from("owner"), None);
assert_eq!(
config.ignored_rules_path_patterns,
super::default_ignored_rules_path_patterns()
);
}
#[test]
fn legacy_machine_config_without_ignored_rules_paths_still_parses() {
let config = facet_json::from_str::<MachineConfig>(
r#"{
"version": 1,
"owner_sid": "owner",
"sync_dir": "C:\\ProgramData\\teamy_mft\\cache",
"pipe_name": "\\\\.\\pipe\\teamy-mft-daemon",
"service_name": "teamy-mft-daemon",
"idle_timeout_secs": 300
}"#,
)
.expect("legacy machine config should parse")
.with_normalized_defaults();
assert_eq!(
config.ignored_rules_path_patterns,
super::default_ignored_rules_path_patterns()
);
assert!(
config
.ignored_rules_path_patterns
.iter()
.any(|pattern| pattern.contains("CARGO_HOME"))
);
assert_eq!(
config.ignored_rules_path_patterns.len(),
DEFAULT_IGNORED_RULES_PATH_PATTERNS.len()
);
}
}