use crate::error::CliError;
use camino::Utf8Path;
use std::fs;
use std::io::Write;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SyncOutcome {
Unchanged,
Updated,
}
pub(super) fn validate_generated(destination: &Utf8Path) -> Result<(), CliError> {
match fs::read(destination) {
Ok(current) if current.starts_with(super::GENERATED_HEADER.as_bytes()) => Ok(()),
Ok(_) => Err(CliError::EmbeddingFileConflict {
path: destination.to_path_buf(),
reason: "the file was not generated by geam embedding sync".to_owned(),
}),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(CliError::FileRead {
path: destination.to_path_buf(),
error,
}),
}
}
pub(super) fn check(
manifest: &Utf8Path,
destination: &Utf8Path,
expected: &[u8],
) -> Result<(), CliError> {
match fs::read(destination) {
Ok(current) if current == expected => Ok(()),
Ok(_) => Err(out_of_date(manifest, destination)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
Err(out_of_date(manifest, destination))
}
Err(error) => Err(CliError::FileRead {
path: destination.to_path_buf(),
error,
}),
}
}
pub(super) fn sync(
directory: &Utf8Path,
destination: &Utf8Path,
expected: &[u8],
) -> Result<SyncOutcome, CliError> {
match fs::read(destination) {
Ok(current) if current == expected => return Ok(SyncOutcome::Unchanged),
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
return Err(CliError::FileRead {
path: destination.to_path_buf(),
error,
});
}
}
let mut temporary =
tempfile::NamedTempFile::new_in(directory).map_err(|error| CliError::FileWrite {
path: destination.to_path_buf(),
error,
})?;
write_expected(&mut temporary, destination, expected).and_then(|()| {
temporary
.persist(destination)
.map(|_| ())
.map_err(|error| CliError::FileWrite {
path: destination.to_path_buf(),
error: error.error,
})
})?;
Ok(SyncOutcome::Updated)
}
fn write_expected(
writer: &mut dyn Write,
destination: &Utf8Path,
expected: &[u8],
) -> Result<(), CliError> {
writer
.write_all(expected)
.and_then(|()| writer.flush())
.map_err(|error| CliError::FileWrite {
path: destination.to_path_buf(),
error,
})
}
fn out_of_date(manifest: &Utf8Path, destination: &Utf8Path) -> CliError {
CliError::EmbeddingBindingsOutOfDate {
manifest: manifest.to_path_buf(),
output: destination.to_path_buf(),
}
}
#[cfg(test)]
mod tests {
use super::{SyncOutcome, check, sync, validate_generated, write_expected};
use crate::error::CliError;
use camino::Utf8PathBuf;
use std::fs;
use std::io::{self, Write};
use tempfile::tempdir;
#[test]
fn permits_only_missing_or_marked_generated_files_for_replacement() {
let directory = tempdir().expect("temporary directory");
let root = Utf8PathBuf::from_path_buf(directory.path().to_owned()).expect("UTF-8 fixture");
let destination = root.join("geam_bindings.rs");
validate_generated(&destination).expect("missing generated file");
fs::write(&destination, "// handwritten Rust\n").expect("user module");
assert_eq!(
validate_generated(&destination)
.expect_err("user-owned module")
.to_string(),
format!(
"refusing to replace existing embedding file {destination}: the file was not generated by geam embedding sync"
)
);
fs::write(
&destination,
"// Generated by `geam embedding sync`. Do not edit.\n\n// old bindings\n",
)
.expect("generated module");
validate_generated(&destination).expect("marked generated module");
let unreadable = root.join("directory");
fs::create_dir(&unreadable).expect("conflicting directory");
assert!(
matches!(validate_generated(&unreadable), Err(CliError::FileRead { path, .. }) if path == unreadable)
);
}
#[test]
fn leaves_identical_output_untouched_and_atomically_replaces_changes() {
let directory = tempdir().expect("temporary directory should be created");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf())
.expect("temporary path should be valid UTF-8");
let destination = root.join("geam_bindings.rs");
fs::write(&destination, "same").expect("fixture output should be written");
#[cfg(unix)]
let original_inode = std::os::unix::fs::MetadataExt::ino(
&fs::metadata(&destination).expect("fixture metadata should be readable"),
);
assert_eq!(
sync(&root, &destination, b"same").expect("identical output should succeed"),
SyncOutcome::Unchanged,
);
#[cfg(unix)]
assert_eq!(
std::os::unix::fs::MetadataExt::ino(
&fs::metadata(&destination).expect("unchanged metadata should be readable"),
),
original_inode,
);
assert_eq!(
sync(&root, &destination, b"changed").expect("changed output should succeed"),
SyncOutcome::Updated,
);
assert_eq!(
fs::read(&destination).expect("updated output should be readable"),
b"changed",
);
#[cfg(unix)]
assert_ne!(
std::os::unix::fs::MetadataExt::ino(
&fs::metadata(&destination).expect("updated metadata should be readable"),
),
original_inode,
);
}
#[test]
fn checks_exact_missing_and_stale_bytes_without_writing() {
let directory = tempdir().expect("temporary directory should be created");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf())
.expect("temporary path should be valid UTF-8");
let manifest = root.join("Cargo.toml");
let destination = root.join("geam_bindings.rs");
let missing = check(&manifest, &destination, b"expected")
.expect_err("missing output should fail checking");
assert!(matches!(
missing,
CliError::EmbeddingBindingsOutOfDate { manifest: path, output }
if path == manifest && output == destination
));
assert!(!destination.exists());
fs::write(&destination, b"stale\r\n").expect("stale output should be written");
let stale_metadata = fs::metadata(&destination).expect("stale metadata should be readable");
let stale = check(&manifest, &destination, b"expected\n")
.expect_err("byte-stale output should fail checking");
assert!(matches!(
stale,
CliError::EmbeddingBindingsOutOfDate { manifest: path, output }
if path == manifest && output == destination
));
assert_eq!(
fs::read(&destination).expect("stale output should remain readable"),
b"stale\r\n",
);
assert_eq!(
fs::metadata(&destination)
.expect("checked metadata should be readable")
.permissions(),
stale_metadata.permissions(),
);
fs::write(&destination, b"expected\n").expect("exact output should be written");
let exact_metadata = fs::metadata(&destination).expect("exact metadata should be readable");
let modified = exact_metadata
.modified()
.expect("exact modification time should be readable");
#[cfg(unix)]
let inode = std::os::unix::fs::MetadataExt::ino(&exact_metadata);
check(&manifest, &destination, b"expected\n").expect("exact output should pass checking");
let checked_metadata =
fs::metadata(&destination).expect("checked exact metadata should be readable");
assert_eq!(
fs::read(&destination).expect("checked exact output should remain readable"),
b"expected\n",
);
assert_eq!(checked_metadata.permissions(), exact_metadata.permissions());
assert_eq!(
checked_metadata
.modified()
.expect("checked modification time should be readable"),
modified,
);
#[cfg(unix)]
assert_eq!(
std::os::unix::fs::MetadataExt::ino(&checked_metadata),
inode,
);
}
#[test]
fn preserves_check_read_failures_with_destination_context() {
let directory = tempdir().expect("temporary directory should be created");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf())
.expect("temporary path should be valid UTF-8");
let manifest = root.join("Cargo.toml");
let destination = root.join("geam_bindings.rs");
fs::create_dir(&destination).expect("directory fixture should be created");
let error = check(&manifest, &destination, b"expected")
.expect_err("unreadable checked output should fail");
assert!(matches!(
error,
CliError::FileRead { path, .. } if path == destination
));
assert!(destination.is_dir());
}
#[test]
fn preserves_read_failures_with_destination_context() {
let directory = tempdir().expect("temporary directory should be created");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf())
.expect("temporary path should be valid UTF-8");
let destination = root.join("geam_bindings.rs");
fs::create_dir(&destination).expect("directory fixture should be created");
let error =
sync(&root, &destination, b"expected").expect_err("unreadable destination should fail");
assert!(matches!(
error,
CliError::FileRead { path, .. } if path == destination
));
assert!(destination.is_dir());
}
#[test]
fn preserves_write_failures_with_destination_context() {
let directory = tempdir().expect("temporary directory should be created");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf())
.expect("temporary path should be valid UTF-8");
let missing_directory = root.join("missing");
let destination = missing_directory.join("geam_bindings.rs");
let error = sync(&missing_directory, &destination, b"expected")
.expect_err("missing output directory should fail");
assert!(matches!(
error,
CliError::FileWrite { path, error }
if path == destination && error.kind() == std::io::ErrorKind::NotFound
));
assert!(!destination.exists());
}
#[test]
fn preserves_content_write_and_flush_failures_with_destination_context() {
enum Failure {
Write,
Flush,
}
struct FailingWriter(Failure);
impl Write for FailingWriter {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
match self.0 {
Failure::Write => Err(io::Error::other("fixture write failure")),
Failure::Flush => Ok(buffer.len()),
}
}
fn flush(&mut self) -> io::Result<()> {
Err(io::Error::other("fixture flush failure"))
}
}
let destination = Utf8PathBuf::from("/workspace/src/geam_bindings.rs");
for failure in [Failure::Write, Failure::Flush] {
let error = write_expected(&mut FailingWriter(failure), &destination, b"expected")
.expect_err("content output failure should be preserved");
assert!(matches!(
error,
CliError::FileWrite { path, error }
if path == destination && error.kind() == io::ErrorKind::Other
));
}
}
#[test]
fn preserves_atomic_persist_failures_with_destination_context() {
let directory = tempdir().expect("temporary directory should be created");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf())
.expect("temporary path should be valid UTF-8");
let destination = root.join("missing/geam_bindings.rs");
let error = sync(&root, &destination, b"expected")
.expect_err("missing destination parent should reject persistence");
assert!(matches!(
error,
CliError::FileWrite { path, error }
if path == destination && error.kind() == io::ErrorKind::NotFound
));
assert!(!destination.exists());
}
#[cfg(unix)]
#[test]
fn preserves_previous_output_when_atomic_replacement_cannot_start() {
use std::os::unix::fs::PermissionsExt;
let directory = tempdir().expect("temporary directory should be created");
let root = Utf8PathBuf::from_path_buf(directory.path().to_path_buf())
.expect("temporary path should be valid UTF-8");
let destination = root.join("geam_bindings.rs");
fs::write(&destination, "previous").expect("previous output should be written");
fs::set_permissions(&root, fs::Permissions::from_mode(0o500))
.expect("output directory should become read-only");
let result = sync(&root, &destination, b"changed");
fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
.expect("output directory permissions should be restored");
let error = result.expect_err("read-only output directory should reject replacement");
assert!(matches!(
error,
CliError::FileWrite { path, error }
if path == destination
&& error.kind() == std::io::ErrorKind::PermissionDenied
));
assert_eq!(
fs::read(&destination).expect("previous output should remain readable"),
b"previous",
);
}
}