use std::io::{Cursor, Read};
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::{debug, info};
use super::LoaderError;
const EMBEDDED_ZIP: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/pasta_scripts.zip"));
const EXPECTED_MD5: &str = env!("PASTA_SCRIPTS_MD5");
const SELF_DEPLOY_REL: &str = "profile/pasta/pasta_scripts";
const MARKER_NAME: &str = ".md5";
static UNIQUE_COUNTER: AtomicU64 = AtomicU64::new(0);
pub(crate) enum SyncOutcome {
Skipped,
Deployed,
}
pub(crate) fn sync_pasta_scripts(base_dir: &Path) -> Result<SyncOutcome, LoaderError> {
let target_dir = base_dir.join(SELF_DEPLOY_REL);
let marker_path = target_dir.join(MARKER_NAME);
if let Ok(marker) = std::fs::read_to_string(&marker_path)
&& marker.trim() == EXPECTED_MD5
{
debug!(
digest = EXPECTED_MD5,
path = %target_dir.display(),
"pasta_scripts up-to-date (fast path); using embedded version"
);
return Ok(SyncOutcome::Skipped);
}
deploy(&target_dir, &marker_path)
}
fn deploy(target_dir: &Path, marker_path: &Path) -> Result<SyncOutcome, LoaderError> {
let parent = target_dir.parent().unwrap_or(target_dir);
std::fs::create_dir_all(parent).map_err(|e| LoaderError::self_deploy(parent, e))?;
let suffix = unique_suffix();
let temp_dir = parent.join(format!(".pasta_scripts.new.{suffix}"));
let backup_dir = parent.join(format!(".pasta_scripts.old.{suffix}"));
let _ = std::fs::remove_dir_all(&temp_dir);
let _ = std::fs::remove_dir_all(&backup_dir);
if let Err(e) = extract_zip_into(&temp_dir) {
let _ = std::fs::remove_dir_all(&temp_dir);
return Err(e);
}
let had_live = target_dir.exists();
if had_live && let Err(e) = std::fs::rename(target_dir, &backup_dir) {
let _ = std::fs::remove_dir_all(&temp_dir);
return Err(LoaderError::self_deploy(target_dir, e));
}
if let Err(e) = std::fs::rename(&temp_dir, target_dir) {
if had_live {
let _ = std::fs::rename(&backup_dir, target_dir);
}
let _ = std::fs::remove_dir_all(&temp_dir);
return Err(LoaderError::self_deploy(target_dir, e));
}
if had_live {
let _ = std::fs::remove_dir_all(&backup_dir);
}
std::fs::write(marker_path, EXPECTED_MD5)
.map_err(|e| LoaderError::self_deploy(marker_path, e))?;
info!(
digest = EXPECTED_MD5,
path = %target_dir.display(),
"pasta_scripts self-deployed (extracted embedded version)"
);
Ok(SyncOutcome::Deployed)
}
fn extract_zip_into(dest: &Path) -> Result<(), LoaderError> {
std::fs::create_dir_all(dest).map_err(|e| LoaderError::self_deploy(dest, e))?;
let mut archive = zip::ZipArchive::new(Cursor::new(EMBEDDED_ZIP))
.map_err(|e| LoaderError::self_deploy(dest, zip_io_error(e)))?;
for i in 0..archive.len() {
let mut entry = archive
.by_index(i)
.map_err(|e| LoaderError::self_deploy(dest, zip_io_error(e)))?;
let rel = match entry.enclosed_name() {
Some(p) => p,
None => continue,
};
let out_path = dest.join(&rel);
if entry.is_dir() {
std::fs::create_dir_all(&out_path)
.map_err(|e| LoaderError::self_deploy(&out_path, e))?;
continue;
}
if let Some(parent) = out_path.parent() {
std::fs::create_dir_all(parent).map_err(|e| LoaderError::self_deploy(parent, e))?;
}
let mut bytes = Vec::with_capacity(entry.size() as usize);
entry
.read_to_end(&mut bytes)
.map_err(|e| LoaderError::self_deploy(&out_path, e))?;
std::fs::write(&out_path, &bytes).map_err(|e| LoaderError::self_deploy(&out_path, e))?;
}
Ok(())
}
fn unique_suffix() -> String {
let pid = std::process::id();
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let counter = UNIQUE_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("{pid}.{nanos}.{counter}")
}
fn zip_io_error(e: zip::result::ZipError) -> std::io::Error {
match e {
zip::result::ZipError::Io(io) => io,
other => std::io::Error::other(other.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::time::SystemTime;
fn snapshot(dir: &Path) -> BTreeMap<PathBuf, (Vec<u8>, SystemTime)> {
let mut map = BTreeMap::new();
collect_snapshot(dir, dir, &mut map);
map
}
fn collect_snapshot(
root: &Path,
current: &Path,
out: &mut BTreeMap<PathBuf, (Vec<u8>, SystemTime)>,
) {
let Ok(read) = std::fs::read_dir(current) else {
return;
};
for entry in read.flatten() {
let path = entry.path();
let meta = std::fs::symlink_metadata(&path).expect("stat");
if meta.is_dir() {
collect_snapshot(root, &path, out);
} else {
let rel = path.strip_prefix(root).unwrap().to_path_buf();
let bytes = std::fs::read(&path).expect("read");
let mtime = meta.modified().expect("mtime");
out.insert(rel, (bytes, mtime));
}
}
}
#[test]
fn match_skips_without_any_write() {
let base = tempfile::tempdir().expect("tempdir");
let target = base.path().join(SELF_DEPLOY_REL);
std::fs::create_dir_all(&target).expect("mkdir target");
std::fs::write(target.join("existing.lua"), b"-- pre-existing\n").expect("write file");
std::fs::write(target.join(MARKER_NAME), EXPECTED_MD5).expect("write marker");
let before = snapshot(&target);
assert!(
before.contains_key(Path::new(MARKER_NAME)),
"precondition: marker exists"
);
let outcome = sync_pasta_scripts(base.path()).expect("sync ok");
match outcome {
SyncOutcome::Skipped => {}
SyncOutcome::Deployed => panic!("expected Skipped on marker match"),
}
let after = snapshot(&target);
assert_eq!(
before, after,
"no writes must occur on fast path (files/contents/mtimes unchanged)"
);
}
#[test]
fn marker_with_surrounding_whitespace_still_skips() {
let base = tempfile::tempdir().expect("tempdir");
let target = base.path().join(SELF_DEPLOY_REL);
std::fs::create_dir_all(&target).expect("mkdir target");
std::fs::write(target.join("existing.lua"), b"-- pre-existing\n").expect("write file");
std::fs::write(target.join(MARKER_NAME), format!(" {EXPECTED_MD5}\r\n"))
.expect("write padded marker");
let before = snapshot(&target);
let outcome = sync_pasta_scripts(base.path()).expect("sync ok");
match outcome {
SyncOutcome::Skipped => {}
SyncOutcome::Deployed => {
panic!("expected Skipped: padded marker must match after trim")
}
}
let after = snapshot(&target);
assert_eq!(before, after, "no writes on trimmed-match fast path");
}
#[test]
fn mismatch_marker_redeploys() {
let base = tempfile::tempdir().expect("tempdir");
let target = base.path().join(SELF_DEPLOY_REL);
std::fs::create_dir_all(&target).expect("mkdir target");
std::fs::write(target.join(MARKER_NAME), "deadbeef-not-the-expected-digest")
.expect("write marker");
let outcome = sync_pasta_scripts(base.path()).expect("sync ok");
match outcome {
SyncOutcome::Deployed => {
let marker =
std::fs::read_to_string(target.join(MARKER_NAME)).expect("read marker");
assert_eq!(marker.trim(), EXPECTED_MD5);
}
SyncOutcome::Skipped => panic!("expected Deployed on marker mismatch"),
}
}
#[test]
fn missing_marker_deploys() {
let base = tempfile::tempdir().expect("tempdir");
let outcome = sync_pasta_scripts(base.path()).expect("sync ok");
match outcome {
SyncOutcome::Deployed => {}
SyncOutcome::Skipped => panic!("expected Deployed when marker missing"),
}
let target = base.path().join(SELF_DEPLOY_REL);
let marker = std::fs::read_to_string(target.join(MARKER_NAME)).expect("read marker");
assert_eq!(marker.trim(), EXPECTED_MD5);
}
#[test]
fn deploy_replaces_content_without_orphan() {
let base = tempfile::tempdir().expect("tempdir");
let target = base.path().join(SELF_DEPLOY_REL);
std::fs::create_dir_all(&target).expect("mkdir target");
std::fs::write(target.join(MARKER_NAME), "stale-digest-not-expected")
.expect("write stale marker");
std::fs::write(
target.join("zzz_orphan.lua"),
b"-- orphan from old version\n",
)
.expect("write orphan");
std::fs::create_dir_all(target.join("ghost_subdir")).expect("mkdir orphan subdir");
std::fs::write(target.join("ghost_subdir/old.lua"), b"-- nested orphan\n")
.expect("write nested orphan");
let outcome = sync_pasta_scripts(base.path()).expect("sync ok");
match outcome {
SyncOutcome::Deployed => {}
SyncOutcome::Skipped => panic!("expected Deployed on stale marker"),
}
assert!(
!target.join("zzz_orphan.lua").exists(),
"orphan file must be removed after deploy (no orphans)"
);
assert!(
!target.join("ghost_subdir").exists(),
"orphan subtree must be removed after deploy"
);
assert!(
target.join("main.lua").exists(),
"known embedded entry main.lua must exist after deploy"
);
let marker = std::fs::read_to_string(target.join(MARKER_NAME)).expect("read marker");
assert_eq!(marker.trim(), EXPECTED_MD5);
}
#[test]
fn marker_written_last_equals_digest() {
let base = tempfile::tempdir().expect("tempdir");
let outcome = sync_pasta_scripts(base.path()).expect("sync ok");
assert!(matches!(outcome, SyncOutcome::Deployed));
let target = base.path().join(SELF_DEPLOY_REL);
let marker = std::fs::read_to_string(target.join(MARKER_NAME)).expect("read marker");
assert_eq!(marker.trim(), EXPECTED_MD5);
let pasta_dir = base.path().join("profile/pasta");
for entry in std::fs::read_dir(&pasta_dir)
.expect("read profile/pasta")
.flatten()
{
let name = entry.file_name();
let name = name.to_string_lossy();
assert!(
!name.starts_with(".pasta_scripts.new.")
&& !name.starts_with(".pasta_scripts.old."),
"no temp/backup leftover siblings; found: {name}"
);
}
}
#[test]
fn scripts_dir_untouched() {
let base = tempfile::tempdir().expect("tempdir");
let scripts = base.path().join("scripts");
std::fs::create_dir_all(&scripts).expect("mkdir scripts");
std::fs::write(scripts.join("main.lua"), b"-- user custom override\n").expect("write");
std::fs::create_dir_all(scripts.join("sub")).expect("mkdir scripts/sub");
std::fs::write(scripts.join("sub/util.lua"), b"-- nested user file\n").expect("write");
let other = base.path().join("descript.txt");
std::fs::write(&other, b"charset,UTF-8\n").expect("write other");
let before = snapshot(&scripts);
let before_other = std::fs::read(&other).expect("read other");
let outcome = sync_pasta_scripts(base.path()).expect("sync ok");
assert!(matches!(outcome, SyncOutcome::Deployed));
let after = snapshot(&scripts);
assert_eq!(before, after, "scripts/ must be untouched by self-deploy");
assert_eq!(
before_other,
std::fs::read(&other).expect("read other after"),
"unrelated ghost files must be untouched"
);
}
#[cfg(windows)]
#[test]
fn deploy_failure_preserves_prior_live_state() {
use std::os::windows::fs::OpenOptionsExt;
let base = tempfile::tempdir().expect("tempdir");
let target = base.path().join(SELF_DEPLOY_REL);
std::fs::create_dir_all(&target).expect("mkdir target");
let sentinel_a = target.join("locked.lua");
let sentinel_b = target.join("keep.lua");
let nested = target.join("sub");
std::fs::create_dir_all(&nested).expect("mkdir nested");
let sentinel_c = nested.join("nested.lua");
std::fs::write(&sentinel_a, b"-- prior live file (locked)\n").expect("write sentinel a");
std::fs::write(&sentinel_b, b"-- prior live file (keep)\n").expect("write sentinel b");
std::fs::write(&sentinel_c, b"-- prior live nested file\n").expect("write sentinel c");
const STALE_MARKER: &str = "stale-digest-prior-live-state";
std::fs::write(target.join(MARKER_NAME), STALE_MARKER).expect("write stale marker");
let scripts = base.path().join("scripts");
std::fs::create_dir_all(&scripts).expect("mkdir scripts");
std::fs::write(scripts.join("main.lua"), b"-- user override\n").expect("write user");
let scripts_before = snapshot(&scripts);
let lock = std::fs::OpenOptions::new()
.read(true)
.share_mode(0) .open(&sentinel_a)
.expect("open exclusive handle inside live dir");
let result = sync_pasta_scripts(base.path());
match result {
Err(LoaderError::SelfDeploy { .. }) => {}
Err(other) => panic!("expected SelfDeploy error, got {other:?}"),
Ok(outcome) => {
let kind = match outcome {
SyncOutcome::Skipped => "Skipped",
SyncOutcome::Deployed => "Deployed",
};
panic!("expected Err(SelfDeploy) when swap is blocked, got Ok({kind})");
}
}
assert!(
sentinel_a.exists(),
"locked sentinel must still exist after failed deploy (live preserved)"
);
assert_eq!(
std::fs::read(&sentinel_b).expect("read sentinel b"),
b"-- prior live file (keep)\n",
"sentinel b must survive failed deploy unchanged"
);
assert_eq!(
std::fs::read(&sentinel_c).expect("read nested sentinel"),
b"-- prior live nested file\n",
"nested sentinel must survive failed deploy unchanged"
);
let marker = std::fs::read_to_string(target.join(MARKER_NAME)).expect("read marker");
assert_eq!(
marker.trim(),
STALE_MARKER,
"stale .md5 must remain unchanged on failed deploy (marker written last)"
);
assert_ne!(
marker.trim(),
EXPECTED_MD5,
"marker must NOT advance to expected digest when deploy failed"
);
let pasta_dir = base.path().join("profile/pasta");
for entry in std::fs::read_dir(&pasta_dir)
.expect("read profile/pasta")
.flatten()
{
let name = entry.file_name();
let name = name.to_string_lossy();
assert!(
!name.starts_with(".pasta_scripts.new.")
&& !name.starts_with(".pasta_scripts.old."),
"no temp/backup leftover after failed deploy; found: {name}"
);
}
let scripts_after = snapshot(&scripts);
assert_eq!(
scripts_before, scripts_after,
"scripts/ must be untouched even on failed deploy"
);
drop(lock);
assert_eq!(
std::fs::read(&sentinel_a).expect("read sentinel a after drop"),
b"-- prior live file (locked)\n",
"locked sentinel must survive failed deploy unchanged"
);
}
}