Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ without acknowledging a deferred head, and [verified directory move recovery](do
that preserves subtree identities, current revisions, and pending journal operations. Applications
retain their own dispatch policy and durable move intents.

[Protected local operations](docs/protected-local-operations.md) retain one original native object
across a short asynchronous callback, same-object local conversion and Access-only descriptor work.
They preserve unuploaded bytes and pending state without marking in sync or acknowledging changes.
The host owns original evidence, permission policy, verification and recovery; directory mode does
not freeze tree membership.

## Quick start

### Requirements
Expand Down
2 changes: 2 additions & 0 deletions docs/_toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
href: sync-root-lifecycle.md
- name: Placeholders and hydration
href: placeholders.md
- name: Protected local operations
href: protected-local-operations.md
- name: Local change feed
href: local-change-feed.md
- name: Remote change application
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ working directly with `cfapi.h` and `CldApi.dll`.
| Upload local changes | [Local change feed](local-change-feed.md) |
| Apply remote metadata changes | [Remote change application](remote-change-application.md) |
| Choose and protect durable state | [SQLite state](state-store-sqlite.md) |
| Initialize one local object's identity and permissions | [Protected local operations](protected-local-operations.md) |
| Check OS and architecture support | [Platform support](platform-support.md) |
| Diagnose a failed operation | [Troubleshooting](troubleshooting.md) |
| Understand branches and releases | [Release and branch model](releasing.md) |
Expand Down
9 changes: 9 additions & 0 deletions docs/platform-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ capabilities use additional build or Cloud Files integration thresholds:
| Placeholder management policy | Integration 784 or later |
| Full-restart hydration and force-convert flags | Integration 1280 or later |
| Hydration range information | Integration 1536 or later |
| Protected local callbacks and Access-only descriptors | Windows build 16299 or later; applicable native data/DACL access and verified same-object sharing |

Callers should feature-detect optional capabilities and handle the reported unsupported result.

Expand All @@ -36,3 +37,11 @@ scripts before treating a local build as a supported result.
The core API is Windows-only. Desktop Shell behavior, MSIX/Desktop Bridge packaging, Explorer
visual evidence, and host registration are validated separately from the library's managed tests.
The host application remains responsible for registration and removal UX.

Protected local file operations reject existing writers, writable mappings and aliases. Local
conversion or file DACL application requires the actual disallowed-placeholder-hardlink policy.
Ordinary-file inspection does not provide hardlink freezing. Cold metadata and DACL work reads no
source content. Directory metadata mode pins one namespace object while members remain free to
change; it provides no tree freeze. Cancellation and short budgets stop new work, then drain actual
callbacks before release. Native driver stalls and uncooperative callbacks can delay completion.
See [Protected local operations](protected-local-operations.md) for receipts and recovery.
138 changes: 138 additions & 0 deletions docs/protected-local-operations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Protected local operations

`CloudItem.RunProtectedLocalOperationAsync` awaits short application work while CfSharp retains
facade path admission, official state-store ownership and the original Windows file object.
Capture and durably retain the original `CloudLocalFileBinding` before beginning an operation.
A replacement's newly captured binding does not establish ownership of the old object.

```csharp
CloudItemSnapshot original = await file.InspectAsync(cancellationToken);
CloudLocalFileBinding binding = original.LocalBinding
?? throw new NotSupportedException("Complete native IDs are required.");

// The application durably records its intent and original binding before calling.
CloudProtectedLocalOperationResult receipt = await file.RunProtectedLocalOperationAsync(
new CloudProtectedLocalOperationRequest(binding),
async (scope, stop) =>
{
CloudItemSnapshot current = await scope.InspectAsync(stop);
// Await only necessary local work. The original object stays protected across await.
await Task.Yield();
current = await file.InspectAsync(stop); // Exact-item calls reuse this scope.
}, cancellationToken);
```

Exclusive file mode uses one library-owned Windows file object with read/write data and DACL
access and share-none. It rejects existing writers and writable mappings; competing data or
namespace opens receive native sharing failures. No content is read and no source provider is
called. This deliberately avoids composing a CFAPI exclusive oplock with a second DACL writer:
that writer can break the oplock and wait for the very reference its caller is retaining.
CfSharp neither borrows an automatically invalidatable opaque pointer nor exposes an owning handle
to application code. This API requires Windows 10 version 1709 or later and applicable native access.
It does not use the existing `CloudItemLease` as an outer wrapper around path-based conversion.

`DirectoryMetadata` mode provides same-object metadata access and pins the directory name;
its members, descendant content and enumeration can change. Exclusive mode on a directory is
`Unsupported`. Neither mode provides whole-tree freezing. Ordinary files have no hardlink-freeze
claim: placeholder registration policy cannot protect ordinary-file aliases.

For a permission initialization that requires placeholder hardlink exclusion, use
`CloudProtectedLocalOperationRequest.ForLocalConversion(binding, localIdentity)`. It checks the
actual root's disallowed-hardlink policy, rejects existing aliases, prepares the exact local
identity and commits its official projection before invoking the callback. The identity must not
contain an accepted remote revision. The native conversion uses the same exclusive file object,
preserving bytes, binding and DACL without marking in sync or dehydrating content.

`scope.ConvertToPlaceholderAsync` provides the same preparation within a callback. An exact-item
`file.ConvertToPlaceholderAsync` call also reuses the scope, with only content-preserving defaults.
Existing placeholder identities must match exactly; this API cannot replace identity or rekey
known pending work. It preserves prior acknowledged revision and local ID fields, journal rows,
conflicts and checkpoints. No source content is read, including when inspecting a cold placeholder.

The receipt distinguishes `NativeConverted`, `NativeIdentityPrepared` and
`DurableProjectionCommitted`, retaining the conversion HRESULT and USN when available. After
native success, required projection completes even if cancellation arrives. A failed projection
returns `NativeAppliedProjectionPending` and prevents a preparation-gated callback from starting.
Keep the original binding and identity, then retry a new request after recovery or restart. A
matching already prepared object needs no second conversion; a replacement is rejected. A
transaction-disposal error after commit retains the true committed fact and its original error.
If application code handles a scoped conversion's projection failure, later file DACL application
in that same scope is still rejected until the required projection has recovered.

Inside the callback, `ReadAccessDescriptorAsync` returns an independent `FileSecurity` or
`DirectorySecurity`. `ApplyAccessDescriptorAsync` snapshots the input, applies only
`AccessControlSections.Access` on the original native file object, then returns a fresh readback.
Owner, group and audit sections are never written, even if supplied. Files require an actual
placeholder under disallowed hardlink policy before application; ordinary-file reads are allowed.
No original handle is exposed, attached to a stream or transferred to application code. Caller
descriptor edits must be synchronized while the input copy is taken.

`AccessDescriptorApplied` and `AccessDescriptorReadBack` record separate actual facts. An exception
after application does not erase the application receipt or imply rollback. Persist the original
DACL before starting and independently compare the readback against the host's policy. Directory
inheritance may affect descendants; this scope does not freeze those descendants or their membership.
No role SIDs, permission policy, original DACL or credentials are stored in the official database.

Use scope methods for protected work. Exact-item `InspectAsync` calls reuse the context.
Other nested facade work, another protected callback and owner disposal from the callback fail
immediately instead of waiting for the admission already held by that callback. Scope methods
serialize their native and state phases and are drained on callback exit, including admitted calls
the callback neglected to await. Escaped contexts reject new work after exit. The callback itself
never owns a SQLite transaction.

Cancellation stops new scope work and requests cooperative callback termination. CfSharp awaits
the callback's actual completion and admitted scope work before releasing native or store resources.
Native mutation boundaries check original request and step cancellation signals directly, so
delayed linked-token propagation cannot start a mutation after cancellation has been requested.
An uncooperative callback or stalled native driver can delay return; there is no safe forced abort.
External owner disposal rejects new public work and drains admitted operations before closing the
state store. Keep callbacks short and local, without network, worker RPC or user interaction.

Requests have a ten-second cooperative budget, including path admission. `WithBudget` returns an
immutable copy with a positive duration of at most one minute. Callback and context tokens combine
caller cancellation, owner shutdown and budget expiry. `scope.CurrentStage` and `scope.IsDraining`
are safe to observe from another thread: cancellation shows `Draining` while actual work retains
the native object, then `Released` after cleanup. Neither expiry nor cancellation forcibly aborts
arbitrary application work. The returned receipt separates cancellation and budget-expiry facts
from native mutation, durable commit and descriptor application. A delayed callback can return
after the budget without weakening protection.

Application cancellation handlers run away from the owner's lifecycle lock and are also drained
before native release. A throwing handler is reported as `CancellationCallbackError`; it does not
throw through owner shutdown or erase the original work outcome. Such handlers must also be short
and must not wait for owner disposal. No additional transaction spans the callback or its handlers.

An admitted scope-method failure remains in the receipt even if application code handles its
exception. Callback-origin errors remain the original exceptions, distinct from translated native
failures. The first failed scope phase is retained while other already admitted calls drain.

The immutable receipt records the observed binding/snapshot, last work stage, callback progress,
error and elapsed lifetime. It does not declare independently verified permissions, whole-tree
readiness, remote acceptance, synchronization confirmation or journal ACK. The application owns
its intent, original DACL, roles, namespace fence, permission verification and recovery policy.

## Host initialization example

The compiled [sample helper](https://github.com/mirrorpulse/cfsharp/blob/develop/samples/CfSharp.SampleProvider/ProtectedLocalInitialization.cs)
uses only public APIs. It begins with an ordinary inspection request, captures the original DACL
under protection, awaits the host's durable-originals delegate, then converts and applies the
host's desired Access descriptor. This ordering records original evidence before any mutation.
Persist intent and complete binding before beginning the scope; serialize descriptor bytes rather
than retaining a mutable .NET object. Local evidence belongs outside the sync root and outside the
official CfSharp state schema. The host verification delegate and its recovery ledger remain
separate from the library receipt. Keep every delegate short and local.

For an already prepared placeholder, a basic inspection request can perform Access-only work
without replacing identity, including placeholders whose identity carries an accepted revision.
Local conversion requests cannot introduce such a revision. A known ordinary item must retain
its official ItemId and RemoteId when selecting a local identity; do not invent a replacement ID.

The isolated NuGet consumer exercises conversion, DACL read/apply/readback, cold metadata and root
directory mode through published package surfaces after trim and Native AOT compilation. Synthetic
Windows x64/ARM64 library evidence does not close installed/MSIX, non-administrator, whole-tree or
MirrorPulse product acceptance. Those checks remain the host application's responsibility.

Direct native mutation and sync-root re-registration must participate in the host's coordination.
The scope protects normal conflicting file opens; it does not provide administrator resistance or
exclusive access to all metadata writers. Retain original evidence and inspect again before later
operations after release, including after Windows converts an edited placeholder back to a file.
Loading
Loading