diff --git a/src/app/materialize.rs b/src/app/materialize.rs index cbf39bd..0ab03b3 100644 --- a/src/app/materialize.rs +++ b/src/app/materialize.rs @@ -489,6 +489,8 @@ async fn download_files_with_isolation( } Err(error) => { tracing::debug!(file_count = batch.len(), error = ?error, "Splitting failed blob request"); + // A failed multi-file request does not identify the bad blob. + // Halving preserves transfers that are independent of it. let midpoint = batch.len() / 2; let mut first_half = batch; let second_half = first_half.split_off(midpoint); diff --git a/src/domain/merkle.rs b/src/domain/merkle.rs index c146f13..80b9ca3 100644 --- a/src/domain/merkle.rs +++ b/src/domain/merkle.rs @@ -103,6 +103,8 @@ fn build_index_node( } else { let mut groups: BTreeMap> = BTreeMap::new(); for (path, entry) in entries { + // Hash paths before branching so unrelated filenames distribute evenly + // without exposing their common textual prefixes in the trie shape. let path_hash = path_hash(&path); let label = path_hash .chars() diff --git a/src/domain/reconciliation.rs b/src/domain/reconciliation.rs index 33f8dd8..f83887b 100644 --- a/src/domain/reconciliation.rs +++ b/src/domain/reconciliation.rs @@ -111,6 +111,8 @@ fn preferred_entry(left: &Entry, right: &Entry) -> Entry { } fn conflict_sort_key(entry: &Entry) -> (&str, &str, i128) { + // Concurrent clocks have no causal winner. This stable total order makes every + // device select the same winner while preserving the other version as a conflict. ( &entry.author_device_id, entry.blob_hash.as_deref().unwrap_or(""), diff --git a/src/filesystem.rs b/src/filesystem.rs index 4f7438f..b420fa0 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -81,6 +81,7 @@ pub fn safe_destination(folder_path: &Path, relative_path: &str) -> anyhow::Resu .components() .any(|component| !matches!(component, Component::Normal(_))) { + // Remote manifest paths must never escape the folder or address a special path. anyhow::bail!("peer sent an unsafe path: {relative_path:?}"); } let destination = folder_path.join(relative_path); diff --git a/src/storage/fingerprints.rs b/src/storage/fingerprints.rs index b990d1e..8edd28f 100644 --- a/src/storage/fingerprints.rs +++ b/src/storage/fingerprints.rs @@ -69,6 +69,8 @@ impl StateStore { ) -> anyhow::Result> { self.connection .query_row( + // `IS` keeps NULL file identifiers comparable across filesystems + // that cannot provide a stable identifier. "SELECT blob_hash FROM file_fingerprints WHERE folder_id = ?1 AND path = ?2 AND size_bytes = ?3 AND modified_at_nanos = ?4 AND file_id IS ?5", params![ folder_id.to_string(), diff --git a/src/storage/manifests.rs b/src/storage/manifests.rs index 9c9defa..a8a5267 100644 --- a/src/storage/manifests.rs +++ b/src/storage/manifests.rs @@ -147,6 +147,8 @@ fn history_payload( manifest: &Manifest, revision_count: u64, ) -> anyhow::Result { + // Deltas keep ordinary revisions compact. Periodic checkpoints bound the + // amount of history needed to reconstruct any retained revision. if previous.is_none() || revision_count.is_multiple_of(HISTORY_CHECKPOINT_INTERVAL) { return Ok(HistoryPayload { kind: CHECKPOINT_RECORD_KIND, diff --git a/src/storage/paths.rs b/src/storage/paths.rs index 5a6107f..92fd454 100644 --- a/src/storage/paths.rs +++ b/src/storage/paths.rs @@ -120,6 +120,8 @@ fn write_private_file(path: &Path, contents: &[u8]) -> anyhow::Result<()> { let mut temporary_file = tempfile::NamedTempFile::new_in(parent)?; restrict_file_permissions(temporary_file.path())?; temporary_file.write_all(contents)?; + // Sync before rename so a successful replacement never leaves a partially + // written identity or state file after a crash. temporary_file.as_file().sync_all()?; temporary_file.persist(path).map_err(|error| error.error)?; restrict_file_permissions(path)