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: 5 additions & 1 deletion crates/nms-cache/src/freshness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ pub struct LoadResult {
///
/// Freshness is determined only by file modification times. A fresh cache is
/// still checked for a supported envelope and model semantic revision; if its
/// read or decode fails, startup warns and rebuilds from the save file.
/// read, decode, or duplicated player reality consistency check fails, startup
/// warns and rebuilds from the save file. This check requires
/// `player.current_reality == player.current_address.reality_index`; it does not
/// compare the independently selected galaxy or previous/freighter addresses.
/// If parsing the source save fails, no replacement cache is written.
pub fn load_or_rebuild(
cache_path: &Path,
save_path: &Path,
Expand Down
11 changes: 11 additions & 0 deletions crates/nms-cache/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,19 @@
//! envelope or model semantics. With caching enabled, [`load_or_rebuild`]
//! checks a fresh cache and, when reading it fails, reparses the source and
//! writes a current envelope.
//! Persisted player state is accepted only when its duplicated physical reality
//! fields agree: `current_reality` must equal
//! `current_address.reality_index`. The independently selected galaxy, previous
//! address, and freighter address are not constrained by this check. The cache
//! reader rejects contradictory state and lets the existing loader rebuild from
//! the source save; it never guesses which cached field should be corrected.
//! Direct [`serialize::deserialize`] and [`serialize::read_cache`] calls return
//! errors for legacy or incompatible caches and do not rebuild them.
//! Direct [`rebuild_model`] calls are infallible and consume caller-supplied
//! [`CacheData`] without persisted-byte validation or source fallback; callers
//! constructing that data are responsible for its consistency. The checked
//! persisted-data entrypoints validate their input, while [`load_or_rebuild`]
//! additionally reparses the source save after a cache read failure.
//!
//! Adding [`CacheError::Incompatible`] can require updates to external code
//! that exhaustively matches `CacheError`. The nested
Expand Down
27 changes: 23 additions & 4 deletions crates/nms-cache/src/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ const CACHE_SCHEMA_REVISION: u32 = 2;
const CACHE_SEMANTIC_REVISION: u32 = 2;
const CACHE_HEADER_LEN: usize = 16;

/// Validate duplicated physical reality fields without changing cache data.
fn validate_player_state(data: &CacheData) -> Result<(), String> {
if let Some(player) = &data.player_state {
let address_reality = player.current_address.reality_index;
if player.current_reality != address_reality {
return Err(format!(
"player state reality mismatch: current_reality={} but current_address.reality_index={address_reality}",
player.current_reality
));
}
}

Ok(())
}

/// Extract cache data from a GalaxyModel.
pub fn extract_cache_data(model: &GalaxyModel, save_version: u32) -> CacheData {
let systems: Vec<CachedSystem> = model
Expand Down Expand Up @@ -78,8 +93,10 @@ pub fn extract_cache_data(model: &GalaxyModel, save_version: u32) -> CacheData {
///
/// # Errors
///
/// Returns [`CacheError::Serialize`] if rkyv cannot encode the cache data.
/// Returns [`CacheError::Serialize`] if the duplicated player reality fields
/// disagree or rkyv cannot encode the cache data.
pub fn serialize(data: &CacheData) -> Result<Vec<u8>, CacheError> {
validate_player_state(data).map_err(CacheError::Serialize)?;
let payload =
rkyv::to_bytes::<RkyvError>(data).map_err(|e| CacheError::Serialize(e.to_string()))?;
let mut bytes = Vec::with_capacity(CACHE_HEADER_LEN + payload.len());
Expand All @@ -103,7 +120,7 @@ pub fn serialize(data: &CacheData) -> Result<Vec<u8>, CacheError> {
/// Returns [`CacheError::Incompatible`] for a truncated header, invalid magic,
/// unsupported schema revision, or unsupported semantic revision. Returns
/// [`CacheError::Deserialize`] when the current envelope contains an invalid
/// rkyv payload.
/// rkyv payload or contradictory player reality fields.
pub fn deserialize(bytes: &[u8]) -> Result<CacheData, CacheError> {
if bytes.len() < CACHE_HEADER_LEN {
return Err(CacheError::Incompatible(
Expand Down Expand Up @@ -143,8 +160,10 @@ pub fn deserialize(bytes: &[u8]) -> Result<CacheData, CacheError> {
// alignment requirements. Copy into aligned storage before checked decode.
let mut aligned = AlignedVec::<16>::with_capacity(bytes.len() - CACHE_HEADER_LEN);
aligned.extend_from_slice(&bytes[CACHE_HEADER_LEN..]);
rkyv::from_bytes::<CacheData, RkyvError>(&aligned)
.map_err(|e| CacheError::Deserialize(e.to_string()))
let data = rkyv::from_bytes::<CacheData, RkyvError>(&aligned)
.map_err(|e| CacheError::Deserialize(e.to_string()))?;
validate_player_state(&data).map_err(CacheError::Deserialize)?;
Ok(data)
}

/// Write cache data to a file.
Expand Down
Loading
Loading