use super::{File, FileBacking, NativeFile};
use crate::file_descriptors::{DescriptorKind, DescriptorPermit, FileOpenContext};
use std::io;
use std::path::Path;
mod control_candidate;
pub use control_candidate::{CapturedControlCandidate, PendingControlPublication};
mod directory_candidate;
pub use directory_candidate::CapturedDirectoryCandidate;
#[cfg(unix)]
#[path = "source_directory/unix.rs"]
mod platform;
#[cfg(windows)]
#[path = "source_directory/windows.rs"]
mod platform;
#[cfg(not(any(unix, windows)))]
mod platform {
use super::*;
pub(super) fn create_directory(_root: &std::fs::File, _name: ChildName<'_>) -> io::Result<()> {
Err(unsupported())
}
pub(super) fn create_control_candidate(
_root: &std::fs::File,
_name: ChildName<'_>,
) -> io::Result<std::fs::File> {
Err(unsupported())
}
pub(super) fn matches_child_file(
_root: &std::fs::File,
_name: ChildName<'_>,
_file: &std::fs::File,
_context: &FileOpenContext,
) -> io::Result<bool> {
Err(unsupported())
}
pub(super) fn open_wal_append_child(
_root: &std::fs::File,
_name: ChildName<'_>,
) -> io::Result<std::fs::File> {
Err(unsupported())
}
pub(super) fn open_mutable_child(
_root: &std::fs::File,
_name: ChildName<'_>,
) -> io::Result<std::fs::File> {
Err(unsupported())
}
pub(super) fn replace_control(
_root: &std::fs::File,
_file: &std::fs::File,
_source: ChildName<'_>,
_target: ChildName<'_>,
) -> io::Result<()> {
Err(unsupported())
}
pub(super) fn open_root(_path: &Path) -> io::Result<std::fs::File> {
Err(unsupported())
}
pub(super) fn open_child(
_root: &std::fs::File,
_name: ChildName<'_>,
) -> io::Result<std::fs::File> {
Err(unsupported())
}
pub(super) fn open_directory(
_root: &std::fs::File,
_name: ChildName<'_>,
) -> io::Result<std::fs::File> {
Err(unsupported())
}
pub(super) fn remove_child(
_root: &std::fs::File,
_name: ChildName<'_>,
_context: &FileOpenContext,
) -> io::Result<()> {
Err(unsupported())
}
pub(super) fn remove_directory(
_root: &std::fs::File,
_name: ChildName<'_>,
_context: &FileOpenContext,
) -> io::Result<()> {
Err(unsupported())
}
pub(super) fn open_lock_file(
_root: &std::fs::File,
_name: ChildName<'_>,
_mode: SourceDirectoryLockMode,
) -> io::Result<std::fs::File> {
Err(unsupported())
}
#[cfg(any(test, feature = "test-support"))]
pub(super) fn open_recorded_lock(
_root: &std::fs::File,
_name: ChildName<'_>,
_create_new: bool,
) -> io::Result<std::fs::File> {
Err(unsupported())
}
pub(super) fn visit_children(
_root: &std::fs::File,
_visitor: &mut dyn FnMut(&str) -> io::Result<()>,
) -> io::Result<()> {
Err(unsupported())
}
pub(super) fn sync_directory(
_root: &std::fs::File,
_context: &FileOpenContext,
) -> io::Result<()> {
Err(unsupported())
}
fn unsupported() -> io::Error {
io::Error::new(
io::ErrorKind::Unsupported,
"native source directory requires Unix or Windows",
)
}
}
#[cfg(any(test, feature = "test-support"))]
mod power_loss_root;
#[cfg(any(test, feature = "test-support"))]
pub(crate) use power_loss_root::PowerLossRoot;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SourceDirectoryIdentity(crate::native_file_identity::NativeIdentity);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceDirectoryLockMode {
ReadExisting,
ReadWriteCreate,
}
#[derive(Debug)]
pub struct SourceDirectory {
root: File,
context: FileOpenContext,
identity: SourceDirectoryIdentity,
capture_id: u64,
cached_children: std::sync::atomic::AtomicBool,
#[cfg(any(test, feature = "test-support"))]
trace: Option<Box<crate::power_loss::NativeDirectoryTrace>>,
}
impl SourceDirectory {
pub fn capture_for_path(path: &Path) -> io::Result<Self> {
Self::capture(path, &crate::file_descriptors::context_for_path(path)?)
}
pub fn operation_context(&self) -> FileOpenContext {
self.context.for_operation()
}
pub fn capture(path: &Path, context: &FileOpenContext) -> io::Result<Self> {
let native = native_scope(context)?;
let permit = context.acquire(DescriptorKind::Transient)?;
#[cfg(any(test, feature = "test-support"))]
let (inner, trace) = match native._guard.upgrade() {
Some(core) => {
let (inner, trace) =
crate::power_loss::NativeDirectoryTrace::capture(core, path, context)?;
(inner, Some(Box::new(trace)))
}
None => (
platform::open_root(path).map_err(|error| context.map_open_error(error))?,
None,
),
};
#[cfg(not(any(test, feature = "test-support")))]
let inner = platform::open_root(path).map_err(|error| context.map_open_error(error))?;
#[cfg(not(any(test, feature = "test-support")))]
let _ = native;
if !inner.metadata()?.is_dir() {
return Err(io::Error::new(
io::ErrorKind::NotADirectory,
"source root is not a directory",
));
}
let identity = SourceDirectoryIdentity(crate::native_file_identity::of_file(&inner)?);
Ok(Self {
identity,
capture_id: next_capture_id()?,
cached_children: std::sync::atomic::AtomicBool::new(false),
root: counted(inner, permit),
context: context.without_inventory(),
#[cfg(any(test, feature = "test-support"))]
trace,
})
}
pub fn identity(&self) -> SourceDirectoryIdentity {
self.identity
}
pub fn same_directory(&self, other: &Self) -> bool {
self.identity.0 == other.identity.0
}
pub fn duplicate(&self, context: &FileOpenContext) -> io::Result<Self> {
self.require_context(context)?;
let scope = native_scope(context)?;
self.require_scope(&scope)?;
let permit = context.acquire(DescriptorKind::Transient)?;
let inner = self
.root
.with_native(std::fs::File::try_clone)
.map_err(|error| context.map_open_error(error))?;
Ok(Self {
root: counted(inner, permit),
context: context.without_inventory(),
identity: self.identity,
capture_id: next_capture_id()?,
cached_children: std::sync::atomic::AtomicBool::new(false),
#[cfg(any(test, feature = "test-support"))]
trace: self.trace.clone(),
})
}
pub fn metadata_bound(&self, child_bytes: usize) -> io::Result<usize> {
let _ = child_bytes;
let bytes = std::mem::size_of::<Self>();
#[cfg(any(test, feature = "test-support"))]
if let Some(trace) = &self.trace {
return bytes
.checked_add(trace.metadata_bound(child_bytes)?)
.ok_or_else(|| io::Error::other("directory metadata bound overflow"));
}
Ok(bytes)
}
pub fn create_child_new(&self, context: &FileOpenContext, name: &str) -> io::Result<File> {
self.open_mutable_child(context, name, true)
}
pub fn shares_project_context(&self, context: &FileOpenContext) -> bool {
std::sync::Arc::ptr_eq(&self.context.state, &context.state)
}
#[doc(hidden)]
pub fn matches_namespace(&self, path: &Path) -> io::Result<bool> {
#[cfg(windows)]
{
Ok(self.same_directory(&Self::capture(path, &self.operation_context())?))
}
#[cfg(unix)]
{
Ok(self.identity.0
== crate::native_file_identity::of_metadata(
&self.operation_context().metadata(path)?,
))
}
#[cfg(not(any(unix, windows)))]
self.root.matches_native_path(path)
}
pub fn open_lock_file(
&self,
context: &FileOpenContext,
name: &str,
mode: SourceDirectoryLockMode,
) -> io::Result<File> {
self.require_context(context)?;
let name = ChildName::new(name)?;
if !name.0.starts_with('.') || !name.0.ends_with(".lock") || name.0.len() < 7 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid source ownership lock name",
));
}
self.open_named_lock(context, name, mode)
}
pub(crate) fn open_database_ownership_lock(
&self,
context: &FileOpenContext,
mode: SourceDirectoryLockMode,
) -> io::Result<File> {
self.open_named_lock(
context,
ChildName::new(crate::ownership::DATABASE_DIRECTORY_LOCK_FILE)?,
mode,
)
}
pub(crate) fn open_catalog_metadata_lock(&self, context: &FileOpenContext) -> io::Result<File> {
self.open_named_lock(
context,
ChildName::new(crate::branch_catalog::METADATA_LOCK_FILE)?,
SourceDirectoryLockMode::ReadWriteCreate,
)
}
fn open_named_lock(
&self,
context: &FileOpenContext,
name: ChildName<'_>,
mode: SourceDirectoryLockMode,
) -> io::Result<File> {
self.require_context(context)?;
let _native = native_scope(context)?;
self.require_scope(&_native)?;
let permit = context.acquire(DescriptorKind::OwnershipLock)?;
let open = || {
self.root
.with_native(|root| platform::open_lock_file(root, name, mode))
.map_err(|error| context.map_open_error(error))
};
#[cfg(any(test, feature = "test-support"))]
let (inner, trace) = match self.trace.as_deref() {
Some(trace) => {
let (inner, trace) = match mode {
SourceDirectoryLockMode::ReadExisting => {
trace.open_child(Path::new(name.0), open)?
}
SourceDirectoryLockMode::ReadWriteCreate => {
trace.open_lock(Path::new(name.0), |create_new| {
self.root
.with_native(|root| {
platform::open_recorded_lock(root, name, create_new)
})
.map_err(|error| context.map_open_error(error))
})?
}
};
(inner, Some(Box::new(trace)))
}
None => (open()?, None),
};
#[cfg(not(any(test, feature = "test-support")))]
let inner = open()?;
if !inner.metadata()?.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"source lock is not a regular file",
));
}
#[cfg(any(test, feature = "test-support"))]
{
Ok(counted_traced(inner, permit, trace))
}
#[cfg(not(any(test, feature = "test-support")))]
{
Ok(counted(inner, permit))
}
}
pub fn open_child(&self, context: &FileOpenContext, name: &str) -> io::Result<File> {
self.open_child_kind(context, name, DescriptorKind::Transient)
}
pub(crate) fn open_child_kind(
&self,
context: &FileOpenContext,
name: &str,
kind: DescriptorKind,
) -> io::Result<File> {
self.open_existing_child(context, name, kind, false)
}
pub fn matches_child_file(
&self,
context: &FileOpenContext,
name: &str,
file: &File,
) -> io::Result<bool> {
self.require_context(context)?;
let name = ChildName::new(name)?;
let scope = native_scope(context)?;
self.require_scope(&scope)?;
self.root.with_native(|root| {
file.with_native(|file| platform::matches_child_file(root, name, file, context))
})
}
pub(crate) fn open_wal_append_child(
&self,
context: &FileOpenContext,
name: &str,
) -> io::Result<File> {
self.open_existing_child(context, name, DescriptorKind::MutableWal, true)
}
fn open_existing_child(
&self,
context: &FileOpenContext,
name: &str,
kind: DescriptorKind,
append: bool,
) -> io::Result<File> {
self.require_context(context)?;
let name = ChildName::new(name)?;
let _native = native_scope(context)?;
self.require_scope(&_native)?;
let permit = context.acquire(kind)?;
let open = || {
self.root
.with_native(|root| {
if append {
platform::open_wal_append_child(root, name)
} else {
platform::open_child(root, name)
}
})
.map_err(|error| context.map_open_error(error))
};
#[cfg(any(test, feature = "test-support"))]
let (inner, trace) = match self.trace.as_deref() {
Some(trace) => {
let (inner, mut trace) = trace.open_child(Path::new(name.0), open)?;
if append {
trace.mark_append();
}
(inner, Some(Box::new(trace)))
}
None => (open()?, None),
};
#[cfg(not(any(test, feature = "test-support")))]
let inner = open()?;
if !inner.metadata()?.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"source child is not a regular file",
));
}
#[cfg(any(test, feature = "test-support"))]
{
Ok(counted_traced(inner, permit, trace))
}
#[cfg(not(any(test, feature = "test-support")))]
{
Ok(counted(inner, permit))
}
}
pub(crate) fn open_mutable_child(
&self,
context: &FileOpenContext,
name: &str,
create_new: bool,
) -> io::Result<File> {
self.require_context(context)?;
let name = ChildName::new(name)?;
let scope = native_scope(context)?;
self.require_scope(&scope)?;
let permit = context.acquire(DescriptorKind::Transient)?;
let open = || {
self.root
.with_native(|root| {
if create_new {
platform::create_control_candidate(root, name)
} else {
platform::open_mutable_child(root, name)
}
})
.map_err(|error| context.map_open_error(error))
};
#[cfg(any(test, feature = "test-support"))]
let (inner, trace) = match self.trace.as_deref() {
Some(trace) => {
let (inner, trace) = if create_new {
trace.create_file(Path::new(name.0), open)?
} else {
trace.open_child(Path::new(name.0), open)?
};
(inner, Some(Box::new(trace)))
}
None => (open()?, None),
};
#[cfg(not(any(test, feature = "test-support")))]
let inner = open()?;
if !create_new && !inner.metadata()?.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"mutable catalog child is not a regular file",
));
}
#[cfg(any(test, feature = "test-support"))]
{
Ok(counted_traced(inner, permit, trace))
}
#[cfg(not(any(test, feature = "test-support")))]
{
Ok(counted(inner, permit))
}
}
pub fn create_directory_new(&self, context: &FileOpenContext, name: &str) -> io::Result<()> {
self.require_context(context)?;
let name = ChildName::new(name)?;
let _native = native_scope(context)?;
self.require_scope(&_native)?;
let _permit = context.acquire(DescriptorKind::Transient)?;
let create = || {
self.root
.with_native(|root| platform::create_directory(root, name))
};
#[cfg(any(test, feature = "test-support"))]
let result = match self.trace.as_deref() {
Some(trace) => trace.create_directory(Path::new(name.0), create),
None => create(),
};
#[cfg(not(any(test, feature = "test-support")))]
let result = create();
result.map_err(|error| context.map_open_error(error))
}
pub fn open_directory(&self, context: &FileOpenContext, name: &str) -> io::Result<Self> {
self.require_context(context)?;
let name = ChildName::new(name)?;
let _native = native_scope(context)?;
self.require_scope(&_native)?;
let permit = context.acquire(DescriptorKind::Transient)?;
let open = || {
self.root
.with_native(|root| platform::open_directory(root, name))
.map_err(|error| context.map_open_error(error))
};
#[cfg(any(test, feature = "test-support"))]
let (inner, trace) = match self.trace.as_deref() {
Some(trace) => {
let (inner, trace) = trace.open_directory(Path::new(name.0), open)?;
(inner, Some(Box::new(trace)))
}
None => (open()?, None),
};
#[cfg(not(any(test, feature = "test-support")))]
let inner = open()?;
if !inner.metadata()?.is_dir() {
return Err(io::Error::new(
io::ErrorKind::NotADirectory,
"source child is not a directory",
));
}
let identity = SourceDirectoryIdentity(crate::native_file_identity::of_file(&inner)?);
Ok(Self {
root: counted(inner, permit),
context: context.without_inventory(),
identity,
capture_id: next_capture_id()?,
cached_children: std::sync::atomic::AtomicBool::new(false),
#[cfg(any(test, feature = "test-support"))]
trace,
})
}
pub fn remove_child(&self, context: &FileOpenContext, name: &str) -> io::Result<()> {
self.require_context(context)?;
let name = ChildName::new(name)?;
let _native = native_scope(context)?;
self.require_scope(&_native)?;
let native = || {
self.root
.with_native(|root| platform::remove_child(root, name, context))
.map_err(|error| context.map_open_error(error))
};
#[cfg(any(test, feature = "test-support"))]
if let Some(trace) = self.trace.as_deref() {
return trace.remove_child(Path::new(name.0), native);
}
native()
}
pub(crate) fn remove_reclaimed_directory(
&self,
context: &FileOpenContext,
name: &str,
expected: &Self,
) -> io::Result<()> {
self.require_context(context)?;
let observed = self.open_directory(context, name)?;
if !observed.same_directory(expected) {
return Err(io::Error::other("reclaimed directory binding changed"));
}
drop(observed);
let name = ChildName::new(name)?;
let scope = native_scope(context)?;
self.require_scope(&scope)?;
let native = || {
self.root
.with_native(|root| platform::remove_directory(root, name, context))
.map_err(|error| context.map_open_error(error))
};
#[cfg(any(test, feature = "test-support"))]
if let Some(trace) = self.trace.as_deref() {
return trace.remove_directory(Path::new(name.0), native);
}
native()
}
pub fn sync(&self, context: &FileOpenContext) -> io::Result<()> {
self.require_context(context)?;
let scope = native_scope(context)?;
self.require_scope(&scope)?;
let native = || {
self.root
.with_native(|root| platform::sync_directory(root, context))
.map_err(|error| context.map_open_error(error))
};
#[cfg(any(test, feature = "test-support"))]
if let Some(trace) = self.trace.as_deref() {
return trace.sync(native);
}
native()
}
pub fn visit_children(
&self,
context: &FileOpenContext,
maximum_entries: usize,
mut visitor: impl FnMut(&str) -> io::Result<()>,
) -> io::Result<()> {
self.require_context(context)?;
let _permit = {
let _native = native_scope(context)?;
self.require_scope(&_native)?;
context.acquire(DescriptorKind::Transient)?
};
let mut remaining = maximum_entries;
self.root
.with_native(|root| {
platform::visit_children(root, &mut |name| {
ChildName::new(name)?;
remaining = remaining.checked_sub(1).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"source directory entry budget exceeded",
)
})?;
visitor(name)
})
})
.map_err(|error| context.map_open_error(error))
}
pub(super) fn capture_id(&self) -> u64 {
self.capture_id
}
pub(super) fn mark_cached_child(&self) {
self.cached_children
.store(true, std::sync::atomic::Ordering::Relaxed);
}
#[doc(hidden)]
pub fn retire_cached_children(&self) {
if self
.cached_children
.load(std::sync::atomic::Ordering::Relaxed)
&& let Some(handles) = self.context.state.existing_immutable_handles()
{
handles.retire_source(self.capture_id);
}
}
fn require_context(&self, context: &FileOpenContext) -> io::Result<()> {
if self.shares_project_context(context) {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"source directory operation requires its project context",
))
}
}
fn require_scope(&self, scope: &NativeScope<'_>) -> io::Result<()> {
#[cfg(any(test, feature = "test-support"))]
match (scope._guard.upgrade(), self.trace.as_deref()) {
(None, None) => {}
(Some(core), Some(trace)) if trace.belongs_to(&core) => {}
_ => {
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"source directory is not bound to this native recorder",
))
}
}
#[cfg(not(any(test, feature = "test-support")))]
let _ = scope;
Ok(())
}
}
impl Drop for SourceDirectory {
fn drop(&mut self) {
self.retire_cached_children();
}
}
fn next_capture_id() -> io::Result<u64> {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
NEXT.fetch_update(
std::sync::atomic::Ordering::Relaxed,
std::sync::atomic::Ordering::Relaxed,
|value| value.checked_add(1),
)
.map_err(|_| io::Error::other("source capture identity exhausted"))
}
pub(super) fn validate_child_name(name: &str) -> io::Result<()> {
ChildName::new(name).map(|_| ())
}
#[cfg(any(test, feature = "test-support"))]
fn counted_traced(
inner: std::fs::File,
permit: DescriptorPermit,
trace: Option<Box<crate::power_loss::NativeTrace>>,
) -> File {
File {
backing: FileBacking::Native(NativeFile {
inner,
permit,
trace,
}),
}
}
fn counted(inner: std::fs::File, permit: DescriptorPermit) -> File {
File {
backing: FileBacking::Native(NativeFile {
inner,
permit,
#[cfg(any(test, feature = "test-support"))]
trace: None,
}),
}
}
#[derive(Clone, Copy)]
struct ChildName<'a>(&'a str);
impl<'a> ChildName<'a> {
fn new(name: &'a str) -> io::Result<Self> {
let name = Self(name);
if name.0.is_empty()
|| name.0.len() > 255
|| matches!(name.0, "." | "..")
|| !name
.0
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid source artifact name",
));
}
Ok(name)
}
}
struct NativeScope<'a> {
#[cfg(any(test, feature = "test-support"))]
_guard: std::sync::MutexGuard<'a, std::sync::Weak<crate::power_loss::ModelCore>>,
#[cfg(not(any(test, feature = "test-support")))]
_borrow: std::marker::PhantomData<&'a FileOpenContext>,
}
fn native_scope(context: &FileOpenContext) -> io::Result<NativeScope<'_>> {
#[cfg(any(test, feature = "test-support"))]
{
let guard = context
.state
.power_loss
.lock()
.map_err(|_| io::Error::other("power-loss context poisoned"))?;
Ok(NativeScope { _guard: guard })
}
#[cfg(not(any(test, feature = "test-support")))]
{
let _ = context;
Ok(NativeScope {
_borrow: std::marker::PhantomData,
})
}
}
#[cfg(all(test, any(unix, windows)))]
mod tests;
#[cfg(all(test, any(target_os = "linux", target_vendor = "apple", windows)))]
mod recording_tests;