From 5af46a5e4a8883493ed80e94b0f9d7b39037e30c Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Fri, 18 Sep 2026 15:25:44 +0100 Subject: [PATCH] fix(app/backend): old instances with bad names break paths --- .../app-lib/src/api/instance/synced_packs/storage.rs | 4 ++-- .../src/state/content_store/commands/read_content.rs | 6 +++--- .../app-lib/src/state/content_store/domain/paths.rs | 12 +++++++++++- packages/app-lib/src/state/content_store/mod.rs | 3 ++- .../state/dirs/move_app_directory/managed_content.rs | 4 ++-- .../src/state/instances/adapters/filesystem.rs | 2 +- .../src/state/instances/commands/create_instance.rs | 11 +++++++---- 7 files changed, 28 insertions(+), 14 deletions(-) diff --git a/packages/app-lib/src/api/instance/synced_packs/storage.rs b/packages/app-lib/src/api/instance/synced_packs/storage.rs index 8c68bf0f3ee..f699c4cb37d 100644 --- a/packages/app-lib/src/api/instance/synced_packs/storage.rs +++ b/packages/app-lib/src/api/instance/synced_packs/storage.rs @@ -1,7 +1,7 @@ use super::{PackLibrary, SyncedPack}; use crate::state::content_store::{ StoredFileHandle, content_file_path, hash_file, input, validate_digest, - validate_relative, + validate_instance_path, validate_relative, }; use crate::state::instances::adapters::sqlite::{content_rows, instance_rows}; use crate::state::{CachedEntry, State}; @@ -360,7 +360,7 @@ async fn recover_legacy_pack( return Ok(stored_file); } for instance in instance_rows::list_instances(&state.pool).await? { - validate_relative(&instance.path)?; + validate_instance_path(&instance.path)?; let base = state.directories.instances_dir().join(&instance.path); if let Some(placement) = library .instances diff --git a/packages/app-lib/src/state/content_store/commands/read_content.rs b/packages/app-lib/src/state/content_store/commands/read_content.rs index c0d8d04871d..3598eee1d1e 100644 --- a/packages/app-lib/src/state/content_store/commands/read_content.rs +++ b/packages/app-lib/src/state/content_store/commands/read_content.rs @@ -7,7 +7,7 @@ use crate::state::content_store::{ ContentStore, FileContent, InstanceFileStorage, ReadableContent, StoredFileHandle, StoredFileMetadata, StoredFileRecord, StoredFileStatus, content_file_path, hash_file_with_progress, input, validate_digest, - validate_relative, + validate_instance_path, validate_relative, }; use crate::state::{InstanceFile, file_modified_at_ns}; use itertools::Itertools; @@ -197,7 +197,7 @@ impl ContentStore { } FileContent::Unmanaged => {} } - validate_relative(instance_path)?; + validate_instance_path(instance_path)?; let relative_path = content_file_path(file); validate_relative(&relative_path)?; let path = self.instance_path(instance_path, &relative_path).await?; @@ -212,7 +212,7 @@ impl ContentStore { instance_path: &str, relative_path: &str, ) -> crate::Result { - validate_relative(instance_path)?; + validate_instance_path(instance_path)?; if !relative_path.is_empty() { validate_relative(relative_path)?; } diff --git a/packages/app-lib/src/state/content_store/domain/paths.rs b/packages/app-lib/src/state/content_store/domain/paths.rs index 911b8651bd0..8d80b92845b 100644 --- a/packages/app-lib/src/state/content_store/domain/paths.rs +++ b/packages/app-lib/src/state/content_store/domain/paths.rs @@ -33,11 +33,21 @@ pub(crate) fn validate_relative(path: &str) -> crate::Result<()> { .components() .any(|part| !matches!(part, Component::Normal(_))) { - return Err(input("Invalid instance-relative path")); + return Err(input(format!("Invalid instance-relative path: {path:?}"))); } Ok(()) } +pub(crate) fn validate_instance_path(path: &str) -> crate::Result<()> { + // Legacy instance names can end in dots or spaces; preserve their on-disk path. + let normalized = path + .split('/') + .map(|part| part.trim_end_matches(['.', ' '])) + .join("/"); + validate_relative(&normalized) + .map_err(|_| input(format!("Invalid instance folder path: {path:?}"))) +} + pub(crate) fn is_managed_content_path(path: &str) -> bool { if validate_relative(path).is_err() { return false; diff --git a/packages/app-lib/src/state/content_store/mod.rs b/packages/app-lib/src/state/content_store/mod.rs index e44dee25416..174eaf794b3 100644 --- a/packages/app-lib/src/state/content_store/mod.rs +++ b/packages/app-lib/src/state/content_store/mod.rs @@ -19,7 +19,8 @@ pub(crate) use commands::{ }; pub(crate) use domain::{ content_file_path, file_path_on_disk, is_managed_content_path, normalize, - object_relative_path, relative_link, validate_digest, validate_relative, + object_relative_path, relative_link, validate_digest, + validate_instance_path, validate_relative, }; use model::StoreIssue; pub(crate) use model::{ diff --git a/packages/app-lib/src/state/dirs/move_app_directory/managed_content.rs b/packages/app-lib/src/state/dirs/move_app_directory/managed_content.rs index 7b02d8ca0e5..53ba93a3f7d 100644 --- a/packages/app-lib/src/state/dirs/move_app_directory/managed_content.rs +++ b/packages/app-lib/src/state/dirs/move_app_directory/managed_content.rs @@ -1,7 +1,7 @@ use crate::state::content_store; use crate::state::content_store::{ FileStorageKind, content_file_path, hash_file, input, sync_directory, - validate_relative, + validate_instance_path, validate_relative, }; use crate::state::content_store::{ remove_instance_file, try_shared_file, validate_parent_directories, @@ -36,7 +36,7 @@ pub(super) async fn prepare( ) -> crate::Result> { let mut moves = Vec::new(); for instance in instance_rows::list_instances(pool).await? { - validate_relative(&instance.path)?; + validate_instance_path(&instance.path)?; let files = content_rows::get_instance_files(&instance.id, pool).await?; for binding in diff --git a/packages/app-lib/src/state/instances/adapters/filesystem.rs b/packages/app-lib/src/state/instances/adapters/filesystem.rs index d62b8b63e8c..e63d147b04f 100644 --- a/packages/app-lib/src/state/instances/adapters/filesystem.rs +++ b/packages/app-lib/src/state/instances/adapters/filesystem.rs @@ -17,7 +17,7 @@ pub(crate) fn scan_content_files( instances_dir: &Path, instance_path: &str, ) -> crate::Result> { - crate::state::content_store::validate_relative(instance_path)?; + crate::state::content_store::validate_instance_path(instance_path)?; let instance_full_path = instances_dir.join(instance_path); let instance_dir = io::canonicalize(instance_full_path)?; let linked_instance = diff --git a/packages/app-lib/src/state/instances/commands/create_instance.rs b/packages/app-lib/src/state/instances/commands/create_instance.rs index 88ca01ad94e..64c172339c1 100644 --- a/packages/app-lib/src/state/instances/commands/create_instance.rs +++ b/packages/app-lib/src/state/instances/commands/create_instance.rs @@ -258,8 +258,11 @@ fn content_source_kind(link: &InstanceLink) -> ContentSourceKind { } fn sanitize_instance_name(input: &str) -> String { - input.replace( - ['/', '\\', '?', '*', ':', '\'', '\"', '|', '<', '>', '!'], - "_", - ) + input + .replace( + ['/', '\\', '?', '*', ':', '\'', '\"', '|', '<', '>', '!'], + "_", + ) + .trim_end_matches(['.', ' ']) + .to_string() }