use std::ffi::OsStr;
use std::fs;
use std::io::{Cursor, Read, Write};
use std::path::{Component, Path, PathBuf};
use std::sync::{Arc, OnceLock};
use anyhow::{Context, Result, anyhow, bail, ensure};
use directories::ProjectDirs;
use flate2::read::GzDecoder;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use tar::{Archive, Builder};
use tracing::info;
use zstd::stream::{read::Decoder as ZstdDecoder, write::Encoder as ZstdEncoder};
use super::postgres_mod::PostgresMod;
use tempfile::TempDir;
const RUNTIME_ARCHIVE_NAME: &str = "pglite-wasi.tar.zst";
const EMBEDDED_RUNTIME_ARCHIVE: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/assets/pglite-wasi.tar.zst"
));
const PGDATA_TEMPLATE_ARCHIVE_NAME: &str = "pgdata-template.tar.zst";
const EMBEDDED_PGDATA_TEMPLATE_ARCHIVE: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/assets/prepopulated/pgdata-template.tar.zst"
));
const EMBEDDED_PGDATA_TEMPLATE_MANIFEST: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/assets/prepopulated/pgdata-template.json"
));
static TEMPLATE_CLUSTER: OnceLock<std::result::Result<Arc<TemplateCluster>, String>> =
OnceLock::new();
const TEMPLATE_RUNTIME_STATE_FILES: &[&str] = &["postmaster.pid", "postmaster.opts"];
#[derive(Debug)]
struct TemplateCluster {
root: PathBuf,
_temp_dir: TempDir,
}
#[derive(Debug, Clone)]
pub struct PglitePaths {
pub pgroot: PathBuf,
pub pgdata: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PgDataTemplate {
pub archive_path: PathBuf,
pub manifest_path: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PgDataTemplateManifest {
pub postgres_version: String,
pub wasm_sha256: String,
pub archive_sha256: String,
#[serde(default)]
pub architecture_independent: bool,
}
impl PglitePaths {
pub fn new(app_qual: (&str, &str, &str)) -> Result<Self> {
let pd = ProjectDirs::from(app_qual.0, app_qual.1, app_qual.2)
.context("could not resolve app data dir")?;
let app_dir = pd.data_dir().to_path_buf();
Ok(Self::with_root(app_dir))
}
pub fn with_root(root: impl Into<PathBuf>) -> Self {
let base = root.into();
let pgroot = base.join("tmp");
let pgdata = pgroot.join("pglite").join("base");
Self { pgroot, pgdata }
}
pub fn with_paths(pgroot: impl Into<PathBuf>, pgdata: impl Into<PathBuf>) -> Self {
Self {
pgroot: pgroot.into(),
pgdata: pgdata.into(),
}
}
pub fn mount_root(&self) -> &Path {
&self.pgroot
}
pub fn with_temp_dir() -> Result<(TempDir, Self)> {
let tmp = TempDir::new().context("create temporary directory")?;
let paths = Self::with_root(tmp.path());
Ok((tmp, paths))
}
fn marker_cluster(&self) -> PathBuf {
self.pgdata.join("PG_VERSION")
}
pub fn is_cluster_initialized(&self) -> bool {
self.marker_cluster().exists()
}
}
fn locate_runtime_module(paths: &PglitePaths) -> Option<(PathBuf, PathBuf)> {
let pglite_dir = paths.pgroot.join("pglite");
if !pglite_dir.exists() {
return None;
}
let pglite_bin_dir = pglite_dir.join("bin");
let module = pglite_bin_dir.join("pglite.wasi");
if !module.exists() {
return None;
}
let share = pglite_dir.join("share").join("postgresql");
if !share.exists() || !share.join("postgres.bki").exists() {
return None;
}
Some((module, pglite_bin_dir))
}
fn ensure_runtime(paths: &PglitePaths) -> Result<bool> {
if locate_runtime_module(paths).is_some() {
return Ok(false);
}
if let Some(parent) = paths.pgroot.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create parent directory {}", parent.display()))?;
} else {
fs::create_dir_all(&paths.pgroot).context("create pgroot dir")?;
}
install_runtime_from_tar(paths)?;
locate_runtime_module(paths).ok_or_else(|| {
anyhow!(
"runtime missing: could not locate module under {} after archive install",
paths.pgroot.display()
)
})?;
Ok(true)
}
fn runtime_tar_path() -> Option<PathBuf> {
if let Ok(path) = std::env::var("PGLITE_OXIDE_RUNTIME_ARCHIVE")
.or_else(|_| std::env::var("PGLITE_OXIDE_RUNTIME_TAR"))
{
let candidate = PathBuf::from(path);
if candidate.exists() {
return Some(candidate);
}
}
None
}
fn install_runtime_from_tar(paths: &PglitePaths) -> Result<bool> {
if let Some(tar_path) = runtime_tar_path() {
info!("installing runtime from tar archive {}", tar_path.display());
let file = fs::File::open(&tar_path)
.with_context(|| format!("open runtime archive {}", tar_path.display()))?;
unpack_runtime_archive_reader(file, &tar_path, &paths.pgroot)?;
} else {
info!("installing embedded runtime archive");
unpack_runtime_archive_reader(
Cursor::new(EMBEDDED_RUNTIME_ARCHIVE),
Path::new(RUNTIME_ARCHIVE_NAME),
&paths.pgroot,
)?;
}
Ok(true)
}
fn unpack_runtime_archive_reader<R: Read>(
reader: R,
archive_path: &Path,
destination: &Path,
) -> Result<()> {
let decoder = ZstdDecoder::new(reader)
.with_context(|| format!("decode zstd runtime archive {}", archive_path.display()))?;
let mut archive = Archive::new(decoder);
for entry in archive
.entries()
.with_context(|| format!("read entries from {}", archive_path.display()))?
{
let mut entry =
entry.with_context(|| format!("read entry from {}", archive_path.display()))?;
let path = entry
.path()
.with_context(|| format!("read entry path from {}", archive_path.display()))?
.into_owned();
let relative = path.strip_prefix("tmp").unwrap_or(&path);
let dest = archive_destination(destination, relative)?;
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create directory {}", parent.display()))?;
}
entry
.unpack(&dest)
.with_context(|| format!("unpack {} to {}", path.display(), dest.display()))?;
}
Ok(())
}
fn archive_destination(root: &Path, archive_path: &Path) -> Result<PathBuf> {
let mut dest = root.to_path_buf();
for component in archive_path.components() {
match component {
Component::CurDir => {}
Component::Normal(part) => dest.push(part),
_ => bail!("unsafe archive path {}", archive_path.display()),
}
}
Ok(dest)
}
fn install_extension_reader<R: Read>(paths: &PglitePaths, reader: R) -> Result<()> {
let mut ar = Archive::new(GzDecoder::new(reader));
let target = paths.pgroot.join("pglite");
std::fs::create_dir_all(&target)
.with_context(|| format!("create extension target {}", target.display()))?;
ar.unpack(&target)
.with_context(|| format!("unpack extension into {}", target.display()))?;
Ok(())
}
pub fn install_extension_archive(paths: &PglitePaths, archive_path: &Path) -> Result<()> {
let file = std::fs::File::open(archive_path)
.with_context(|| format!("open extension archive {}", archive_path.display()))?;
install_extension_reader(paths, file)
}
pub fn install_extension_bytes(paths: &PglitePaths, bytes: &[u8]) -> Result<()> {
install_extension_reader(paths, std::io::Cursor::new(bytes))
}
pub fn build_pgdata_template(output_dir: impl AsRef<Path>) -> Result<PgDataTemplate> {
let output_dir = output_dir.as_ref();
fs::create_dir_all(output_dir)
.with_context(|| format!("create template output dir {}", output_dir.display()))?;
let work_dir = output_dir.join(".build");
if work_dir.exists() {
fs::remove_dir_all(&work_dir)
.with_context(|| format!("remove stale template build dir {}", work_dir.display()))?;
}
let paths = PglitePaths::with_root(&work_dir);
let _outcome = install_into_internal_with_template(paths.clone(), false)?;
ensure_cluster_with_template(&paths, false)?;
let (module_path, _) = locate_runtime_module(&paths).ok_or_else(|| {
anyhow!(
"runtime missing: could not locate module under {}",
paths.pgroot.display()
)
})?;
let postgres_version = fs::read_to_string(paths.pgdata.join("PG_VERSION"))
.context("read generated template PG_VERSION")?
.trim()
.to_string();
let archive_path = output_dir.join(PGDATA_TEMPLATE_ARCHIVE_NAME);
write_pgdata_template_archive(&paths.pgdata, &archive_path)?;
let manifest = PgDataTemplateManifest {
postgres_version,
wasm_sha256: sha256_file(&module_path)?,
archive_sha256: sha256_file(&archive_path)?,
architecture_independent: true,
};
let manifest_path = output_dir.join("pgdata-template.json");
fs::write(&manifest_path, serde_json::to_vec_pretty(&manifest)?)
.with_context(|| format!("write template manifest {}", manifest_path.display()))?;
fs::remove_dir_all(&work_dir)
.with_context(|| format!("remove template build dir {}", work_dir.display()))?;
Ok(PgDataTemplate {
archive_path,
manifest_path,
})
}
fn try_install_embedded_pgdata_template(paths: &PglitePaths, module_path: &Path) -> Result<bool> {
if paths.marker_cluster().exists() {
return Ok(false);
}
let manifest: PgDataTemplateManifest =
serde_json::from_slice(EMBEDDED_PGDATA_TEMPLATE_MANIFEST)
.context("parse embedded PGDATA template manifest")?;
ensure!(
manifest.architecture_independent,
"embedded PGDATA template manifest must set architectureIndependent=true"
);
let actual_wasm = sha256_file(module_path)?;
ensure!(
actual_wasm.eq_ignore_ascii_case(&manifest.wasm_sha256),
"embedded PGDATA template wasm hash mismatch: manifest={} actual={actual_wasm}",
manifest.wasm_sha256
);
let actual_archive = sha256_hex(EMBEDDED_PGDATA_TEMPLATE_ARCHIVE);
ensure!(
actual_archive.eq_ignore_ascii_case(&manifest.archive_sha256),
"embedded PGDATA template archive hash mismatch: manifest={} actual={actual_archive}",
manifest.archive_sha256
);
let staging = paths.pgdata.with_file_name(format!(
".pgdata-template-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_nanos())
.unwrap_or_default()
));
if staging.exists() {
fs::remove_dir_all(&staging)
.with_context(|| format!("remove stale template staging {}", staging.display()))?;
}
fs::create_dir_all(&staging)
.with_context(|| format!("create template staging {}", staging.display()))?;
if let Err(err) = unpack_pgdata_template_archive(EMBEDDED_PGDATA_TEMPLATE_ARCHIVE, &staging) {
let _ = fs::remove_dir_all(&staging);
return Err(err);
}
remove_template_runtime_state(&staging)?;
let pg_version = fs::read_to_string(staging.join("PG_VERSION"))
.with_context(|| format!("read {}", staging.join("PG_VERSION").display()))?;
ensure!(
pg_version.trim() == manifest.postgres_version.trim(),
"embedded PGDATA template postgres version mismatch: manifest={} actual={}",
manifest.postgres_version,
pg_version.trim()
);
ensure!(
staging.join("global").join("pg_control").exists(),
"embedded PGDATA template did not contain global/pg_control at archive root"
);
if let Some(parent) = paths.pgdata.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create pgdata parent {}", parent.display()))?;
}
if paths.pgdata.exists() {
fs::remove_dir_all(&paths.pgdata)
.with_context(|| format!("remove existing pgdata {}", paths.pgdata.display()))?;
}
fs::rename(&staging, &paths.pgdata).with_context(|| {
format!(
"promote PGDATA template {} -> {}",
staging.display(),
paths.pgdata.display()
)
})?;
Ok(true)
}
fn unpack_pgdata_template_archive(bytes: &[u8], destination: &Path) -> Result<()> {
let decoder = ZstdDecoder::new(Cursor::new(bytes)).context("decode PGDATA template archive")?;
let mut archive = Archive::new(decoder);
for entry in archive
.entries()
.context("read entries from PGDATA template archive")?
{
let mut entry = entry.context("read PGDATA template archive entry")?;
let path = entry
.path()
.context("read PGDATA template archive entry path")?
.into_owned();
let dest = archive_destination(destination, &path)?;
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create directory {}", parent.display()))?;
}
entry
.unpack(&dest)
.with_context(|| format!("unpack PGDATA template {}", path.display()))?;
}
Ok(())
}
fn write_pgdata_template_archive(pgdata: &Path, archive_path: &Path) -> Result<()> {
let file = fs::File::create(archive_path)
.with_context(|| format!("create template archive {}", archive_path.display()))?;
let mut encoder = ZstdEncoder::new(file, 19)
.with_context(|| format!("create zstd encoder for {}", archive_path.display()))?;
{
let mut builder = Builder::new(&mut encoder);
append_pgdata_template_dir(&mut builder, pgdata, Path::new(""))?;
builder
.finish()
.with_context(|| format!("finish tar archive {}", archive_path.display()))?;
}
encoder
.finish()
.with_context(|| format!("finish zstd archive {}", archive_path.display()))?;
Ok(())
}
fn append_pgdata_template_dir<W: Write>(
builder: &mut Builder<W>,
source: &Path,
archive_prefix: &Path,
) -> Result<()> {
for entry in
fs::read_dir(source).with_context(|| format!("read directory {}", source.display()))?
{
let entry = entry.with_context(|| format!("read entry under {}", source.display()))?;
let file_name = entry.file_name();
if should_skip_template_entry(&file_name) {
continue;
}
let source_path = entry.path();
let archive_path = archive_prefix.join(&file_name);
let file_type = entry
.file_type()
.with_context(|| format!("stat {}", source_path.display()))?;
if file_type.is_dir() {
builder
.append_dir(&archive_path, &source_path)
.with_context(|| format!("append directory {}", archive_path.display()))?;
append_pgdata_template_dir(builder, &source_path, &archive_path)?;
} else if file_type.is_file() {
builder
.append_path_with_name(&source_path, &archive_path)
.with_context(|| format!("append file {}", archive_path.display()))?;
}
}
Ok(())
}
fn remove_template_runtime_state(pgdata: &Path) -> Result<()> {
for name in TEMPLATE_RUNTIME_STATE_FILES {
let path = pgdata.join(name);
if path.exists() {
fs::remove_file(&path).with_context(|| format!("remove {}", path.display()))?;
}
}
Ok(())
}
fn sha256_file(path: &Path) -> Result<String> {
let bytes = fs::read(path).with_context(|| format!("read {}", path.display()))?;
Ok(sha256_hex(&bytes))
}
fn sha256_hex(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
format!("{:x}", hasher.finalize())
}
fn ensure_pgdata(paths: &PglitePaths) -> Result<()> {
if !paths.pgdata.exists() {
fs::create_dir_all(&paths.pgdata).with_context(|| {
format!(
"failed to create initial pgdata directory at {}",
paths.pgdata.display()
)
})?;
}
Ok(())
}
pub fn ensure_cluster(paths: &PglitePaths) -> Result<()> {
ensure_cluster_with_template(paths, true)
}
fn ensure_cluster_with_template(paths: &PglitePaths, use_template: bool) -> Result<()> {
if paths.marker_cluster().exists() {
return Ok(());
}
ensure_runtime(paths)?;
if use_template {
let (module_path, _) = locate_runtime_module(paths).ok_or_else(|| {
anyhow!(
"runtime missing: could not locate module under {} after install",
paths.pgroot.display()
)
})?;
if try_install_embedded_pgdata_template(paths, &module_path)? {
return Ok(());
}
}
ensure_pgdata(paths)?;
let mut pg = PostgresMod::new(paths.clone())?;
pg.ensure_cluster()
}
pub fn preload_runtime_module(paths: &PglitePaths) -> Result<()> {
ensure_runtime(paths)?;
let (module_path, _) = locate_runtime_module(paths).ok_or_else(|| {
anyhow!(
"runtime missing: could not locate module under {}",
paths.pgroot.display()
)
})?;
PostgresMod::preload_module(&module_path)
}
#[derive(Debug)]
pub struct InstallOutcome {
pub paths: PglitePaths,
pub unpacked_runtime: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct InstallOptions {
pub ensure_cluster: bool,
}
impl Default for InstallOptions {
fn default() -> Self {
Self {
ensure_cluster: true,
}
}
}
#[derive(Debug, Clone)]
pub struct MountInfo {
mount: PathBuf,
paths: PglitePaths,
reused_existing: bool,
}
impl MountInfo {
pub fn into_paths(self) -> PglitePaths {
self.paths
}
pub fn mount(&self) -> &Path {
&self.mount
}
pub fn paths(&self) -> &PglitePaths {
&self.paths
}
pub fn reused_existing(&self) -> bool {
self.reused_existing
}
}
pub fn install_default(app_id: (&str, &str, &str)) -> Result<InstallOutcome> {
let paths = PglitePaths::new(app_id)?;
install_into_internal(paths)
}
pub fn install_into(root: &Path) -> Result<InstallOutcome> {
let paths = PglitePaths::with_root(root);
install_into_internal(paths)
}
pub(crate) fn install_temporary_from_template() -> Result<(TempDir, InstallOutcome)> {
let template = template_cluster()?;
let temp_dir = TempDir::new().context("create temporary pglite directory")?;
copy_dir_filtered(&template.root, temp_dir.path())?;
let outcome = InstallOutcome {
paths: PglitePaths::with_root(temp_dir.path()),
unpacked_runtime: false,
};
Ok((temp_dir, outcome))
}
fn install_into_internal(paths: PglitePaths) -> Result<InstallOutcome> {
install_into_internal_with_template(paths, true)
}
fn install_into_internal_with_template(
paths: PglitePaths,
use_template: bool,
) -> Result<InstallOutcome> {
let unpacked_runtime = ensure_runtime(&paths)?;
if use_template && !paths.marker_cluster().exists() {
let (module_path, _) = locate_runtime_module(&paths).ok_or_else(|| {
anyhow!(
"runtime missing: could not locate module under {} after install",
paths.pgroot.display()
)
})?;
try_install_embedded_pgdata_template(&paths, &module_path)?;
}
ensure_pgdata(&paths)?;
Ok(InstallOutcome {
paths,
unpacked_runtime,
})
}
pub fn install_and_init(app_id: (&str, &str, &str)) -> Result<MountInfo> {
let outcome = install_default(app_id)?;
if !outcome.paths.marker_cluster().exists() {
ensure_cluster(&outcome.paths)?;
}
Ok(MountInfo {
mount: outcome.paths.pgroot.clone(),
paths: outcome.paths,
reused_existing: !outcome.unpacked_runtime,
})
}
pub fn install_and_init_in<P: AsRef<Path>>(root: P) -> Result<MountInfo> {
let outcome = install_into(root.as_ref())?;
if !outcome.paths.marker_cluster().exists() {
ensure_cluster(&outcome.paths)?;
}
Ok(MountInfo {
mount: outcome.paths.pgroot.clone(),
paths: outcome.paths,
reused_existing: !outcome.unpacked_runtime,
})
}
pub fn install_with_options(paths: PglitePaths, options: InstallOptions) -> Result<MountInfo> {
let unpacked_runtime = ensure_runtime(&paths)?;
if options.ensure_cluster && !paths.marker_cluster().exists() {
ensure_cluster(&paths)?;
} else {
ensure_pgdata(&paths)?;
}
Ok(MountInfo {
mount: paths.pgroot.clone(),
paths,
reused_existing: !unpacked_runtime,
})
}
fn template_cluster() -> Result<Arc<TemplateCluster>> {
TEMPLATE_CLUSTER
.get_or_init(|| {
build_template_cluster()
.map(Arc::new)
.map_err(|err| format!("{err:#}"))
})
.clone()
.map_err(|message| anyhow!(message))
}
fn build_template_cluster() -> Result<TemplateCluster> {
let temp_dir = TempDir::new().context("create pglite template cluster directory")?;
let outcome = install_into(temp_dir.path())?;
ensure_cluster(&outcome.paths)?;
Ok(TemplateCluster {
root: temp_dir.path().to_path_buf(),
_temp_dir: temp_dir,
})
}
fn copy_dir_filtered(src: &Path, dest: &Path) -> Result<()> {
fs::create_dir_all(dest).with_context(|| format!("create directory {}", dest.display()))?;
for entry in fs::read_dir(src).with_context(|| format!("read directory {}", src.display()))? {
let entry = entry.with_context(|| format!("read entry under {}", src.display()))?;
let file_name = entry.file_name();
if should_skip_template_entry(&file_name) {
continue;
}
let src_path = entry.path();
let dest_path = dest.join(&file_name);
let file_type = entry
.file_type()
.with_context(|| format!("stat {}", src_path.display()))?;
if file_type.is_dir() {
copy_dir_filtered(&src_path, &dest_path)?;
} else if file_type.is_file() {
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create directory {}", parent.display()))?;
}
fs::copy(&src_path, &dest_path).with_context(|| {
format!("copy {} to {}", src_path.display(), dest_path.display())
})?;
} else if file_type.is_symlink() {
copy_symlink(&src_path, &dest_path)?;
}
}
Ok(())
}
fn should_skip_template_entry(file_name: &OsStr) -> bool {
let name = file_name.to_string_lossy();
name.starts_with(".s.PGSQL.") || TEMPLATE_RUNTIME_STATE_FILES.contains(&name.as_ref())
}
#[cfg(unix)]
fn copy_symlink(src: &Path, dest: &Path) -> Result<()> {
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create directory {}", parent.display()))?;
}
let target = fs::read_link(src).with_context(|| format!("read symlink {}", src.display()))?;
std::os::unix::fs::symlink(&target, dest)
.with_context(|| format!("create symlink {} -> {}", dest.display(), target.display()))?;
Ok(())
}
#[cfg(not(unix))]
fn copy_symlink(src: &Path, dest: &Path) -> Result<()> {
let target = fs::read_link(src).with_context(|| format!("read symlink {}", src.display()))?;
let target_path = if target.is_absolute() {
target
} else {
src.parent().unwrap_or_else(|| Path::new(".")).join(target)
};
if target_path.is_dir() {
copy_dir_filtered(&target_path, dest)
} else {
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("create directory {}", parent.display()))?;
}
fs::copy(&target_path, dest)
.with_context(|| format!("copy {} to {}", target_path.display(), dest.display()))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn template_copy_keeps_cluster_files_and_skips_runtime_state() -> Result<()> {
let source = TempDir::new()?;
let pgdata = source.path().join("tmp/pglite/base");
fs::create_dir_all(&pgdata)?;
fs::write(pgdata.join("PG_VERSION"), b"17\n")?;
fs::write(pgdata.join("postmaster.pid"), b"stale pid")?;
fs::write(pgdata.join("postmaster.opts"), b"stale opts")?;
fs::write(source.path().join(".s.PGSQL.5432"), b"socket")?;
fs::write(source.path().join(".s.PGSQL.5432.lock"), b"lock")?;
let dest = TempDir::new()?;
copy_dir_filtered(source.path(), dest.path())?;
assert!(dest.path().join("tmp/pglite/base/PG_VERSION").exists());
assert!(!dest.path().join("tmp/pglite/base/postmaster.pid").exists());
assert!(!dest.path().join("tmp/pglite/base/postmaster.opts").exists());
assert!(!dest.path().join(".s.PGSQL.5432").exists());
assert!(!dest.path().join(".s.PGSQL.5432.lock").exists());
Ok(())
}
#[test]
fn embedded_pgdata_template_installs_valid_cluster() -> Result<()> {
let temp_dir = TempDir::new()?;
let paths = PglitePaths::with_root(temp_dir.path());
ensure_runtime(&paths)?;
let (module_path, _) =
locate_runtime_module(&paths).context("runtime module should be installed")?;
assert!(try_install_embedded_pgdata_template(&paths, &module_path)?);
assert!(paths.pgdata.join("PG_VERSION").exists());
assert!(paths.pgdata.join("global/pg_control").exists());
assert!(!paths.pgdata.join("postmaster.pid").exists());
Ok(())
}
#[test]
fn archive_destination_rejects_parent_components() {
let err = archive_destination(Path::new("/tmp/root"), Path::new("../escape"))
.expect_err("parent components must be rejected");
assert!(err.to_string().contains("unsafe archive path"));
}
}