diff --git a/core/crates/solstone-core-facets/src/store/error.rs b/core/crates/solstone-core-facets/src/store/error.rs index ecd314272..bf24a4aa1 100644 --- a/core/crates/solstone-core-facets/src/store/error.rs +++ b/core/crates/solstone-core-facets/src/store/error.rs @@ -18,6 +18,7 @@ pub enum FacetStoreError { Path(PathError), DeclarationNotObject { path: PathBuf }, EntityLinkNotObject { path: PathBuf }, + CorruptCompletionMarker { path: PathBuf }, } impl fmt::Display for FacetStoreError { @@ -39,6 +40,11 @@ impl fmt::Display for FacetStoreError { path.display() ) } + Self::CorruptCompletionMarker { path } => write!( + formatter, + "facet entity-link repair completion marker is empty or malformed: {}", + path.display() + ), } } } @@ -48,7 +54,9 @@ impl Error for FacetStoreError { match self { Self::Read(error) => Some(error), Self::Path(error) => Some(error), - Self::DeclarationNotObject { .. } | Self::EntityLinkNotObject { .. } => None, + Self::DeclarationNotObject { .. } + | Self::EntityLinkNotObject { .. } + | Self::CorruptCompletionMarker { .. } => None, } } } diff --git a/core/crates/solstone-core-facets/src/store/repair.rs b/core/crates/solstone-core-facets/src/store/repair.rs index c24115c32..c9ccc43fd 100644 --- a/core/crates/solstone-core-facets/src/store/repair.rs +++ b/core/crates/solstone-core-facets/src/store/repair.rs @@ -14,7 +14,7 @@ use solstone_core_entity::{ }; use solstone_core_journal_io::{ AtomicWriteError, DirEntryKind, JsonWriteOptions, PathError, ReadError, list_dir_entries, - path_lexists, read_json, write_json, + path_lexists, read_text, write_json, }; use crate::{FacetTrustLockError, hold_facet_trust_lock}; @@ -620,11 +620,16 @@ fn write_marker(path: &Path, report: &T) -> Result<(), AtomicWrite } fn read_facet_marker(path: &Path) -> Result { - let marker: FacetEntityLinkCompletionMarker = read_json( - path, - marker_default(), - solstone_core_journal_io::MalformedPolicy::Raise, - )?; + let contents = read_text(path, String::new())?; + if contents.trim().is_empty() { + return Err(FacetStoreError::CorruptCompletionMarker { + path: path.to_path_buf(), + }); + } + let marker: FacetEntityLinkCompletionMarker = + serde_json::from_str(&contents).map_err(|_| FacetStoreError::CorruptCompletionMarker { + path: path.to_path_buf(), + })?; let FacetEntityLinkCompletionMarker { completed_at, report, @@ -633,17 +638,6 @@ fn read_facet_marker(path: &Path) -> Result FacetEntityLinkCompletionMarker { - FacetEntityLinkCompletionMarker { - completed_at: String::new(), - report: FacetEntityLinkReport { - facet: String::new(), - branches: Vec::new(), - completion_marker: PathBuf::new(), - }, - } -} - fn is_unparseable_link(error: &FacetStoreError) -> bool { matches!( error, diff --git a/core/crates/solstone-core-facets/src/store_tests.rs b/core/crates/solstone-core-facets/src/store_tests.rs index c35af0be5..3a7137dff 100644 --- a/core/crates/solstone-core-facets/src/store_tests.rs +++ b/core/crates/solstone-core-facets/src/store_tests.rs @@ -14,7 +14,7 @@ use solstone_core_entity::{ }; use crate::{ - FacetEntityLinkRepairBranch, FacetEntityLinkRepairError, create_facet, + FacetEntityLinkRepairBranch, FacetEntityLinkRepairError, FacetStoreError, create_facet, list_facet_entity_directories, read_activity_file, read_facet_declaration, read_facet_entity_link, read_facet_entity_observations, read_log_file, read_news_file, read_todo_file, rename_facet, repair_facet_entity_links, @@ -712,6 +712,35 @@ fn journal_wide_link_repair_reuses_completed_facet_markers_without_rescanning() ); } +#[test] +fn journal_wide_link_repair_rejects_a_corrupt_per_facet_marker_instead_of_trusting_it() { + let temporary = TempDir::new(); + create_test_facet(temporary.path(), "work"); + write_journal_entity(temporary.path(), "person", None); + write_facet_relationship(temporary.path(), "work", "person", json!({})); + let marker = facet_marker_path(temporary.path(), "work"); + fs::create_dir_all(marker.parent().unwrap()).unwrap(); + fs::write(&marker, "").unwrap(); + + let error = repair_facet_entity_links_journal_wide(temporary.path()).unwrap_err(); + + assert!(matches!( + error, + FacetEntityLinkRepairError::CachedMarkerRead { facet, source, .. } + if facet == "work" + && matches!( + source.as_ref(), + FacetStoreError::CorruptCompletionMarker { path } if path == &marker + ) + )); + assert!( + relationship_value(temporary.path(), "work", "person") + .get("entity_id") + .is_none() + ); + assert!(!journal_marker_path(temporary.path()).exists()); +} + fn declaration_value(root: &Path, facet_dir: &str) -> Value { serde_json::from_str( &fs::read_to_string(root.join("facets").join(facet_dir).join("facet.json")).unwrap(),