From 7dd4dfe6222568acb8f8eb00973a9a9be4afc6fb Mon Sep 17 00:00:00 2001 From: Stefan Schnitzler Date: Wed, 2 Sep 2026 17:23:57 +0200 Subject: [PATCH] Update the ".." entry when a directory is moved to a new parent When a directory is renamed into a different parent directory, fx_directory_rename relinks the entry in both parents but never updates the ".." entry inside the moved directory, which keeps pointing at the old parent's starting cluster. A ".." path component inside the moved directory then resolves to the old parent and, once that cluster is reused, to an unrelated place. fx_media_check does not detect this, as it skips the first two entries of every sub-directory. Read the moved directory's ".." entry after the relink and rewrite its starting cluster in place when the parent changed (0 when the new parent is the root). The write happens inside the existing fault tolerant transaction; a rename within one parent ends without the additional write. The regression test extension covers every parent transition - between sub-directories, into and out of the root - and pins the write count: one additional directory entry write for a move, none for a rename within one parent. Fixes Issue #98 Co-Authored-By: Claude Fable 5 --- common/src/fx_directory_rename.c | 85 ++++++- .../filex_directory_rename_test.c | 225 ++++++++++++++++++ 2 files changed, 308 insertions(+), 2 deletions(-) diff --git a/common/src/fx_directory_rename.c b/common/src/fx_directory_rename.c index 48b6784..aeee9bd 100644 --- a/common/src/fx_directory_rename.c +++ b/common/src/fx_directory_rename.c @@ -51,6 +51,8 @@ /* If found, the rename request is valid and the directory will be */ /* changed to the new name. Otherwise, if the directory is not found, */ /* the appropriate error code is returned to the caller. */ +/* If the directory is moved under a different parent, the ".." */ +/* entry inside the directory is updated to name the new parent. */ /* */ /* INPUT */ /* */ @@ -64,6 +66,7 @@ /* */ /* CALLS */ /* */ +/* _fx_directory_entry_read Read entries from directory */ /* _fx_directory_entry_write Write the new directory entry */ /* _fx_directory_free_search Search for a free directory */ /* entry */ @@ -90,6 +93,7 @@ FX_DIR_ENTRY new_dir_entry; FX_DIR_ENTRY search_directory; CHAR *new_name_ptr; ULONG i; +ULONG parent_cluster; CHAR *work_ptr; CHAR alpha, beta; #ifdef FX_RENAME_PATH_INHERIT @@ -451,20 +455,97 @@ UINT j; /* Now wipe out the old directory entry. */ status = _fx_directory_entry_write(media_ptr, &old_dir_entry); + /* Determine if the write was successful. */ + if (status != FX_SUCCESS) + { + #ifdef FX_ENABLE_FAULT_TOLERANT - /* Check for a bad status. */ + FX_FAULT_TOLERANT_TRANSACTION_FAIL(media_ptr); +#endif /* FX_ENABLE_FAULT_TOLERANT */ + + /* Release media protection. */ + FX_UNPROTECT + + /* Return the error code. */ + return(status); + } + + /* The renamed directory itself contains a ".." entry, which stores + the starting cluster of its parent directory. If the rename moved + the directory under a different parent, that stored cluster is + stale now: ".." inside the renamed directory would still lead to + the old parent and, once the old parent's cluster is reused, to an + unrelated place. Update it in place. */ + + /* Determine the starting cluster of the new parent directory. + An empty name in the search directory marks the root + directory - the name is the only root indication, the other + fields keep earlier values in that case. A ".." entry stores + its parent's starting cluster; when the parent is the root + directory, it stores cluster 0. */ + if (search_directory.fx_dir_entry_name[0]) + { + + /* New parent is a sub-directory. */ + parent_cluster = search_directory.fx_dir_entry_cluster; + } + else + { + + /* New parent is the root directory. */ + parent_cluster = 0; + } + + /* old_dir_entry is no longer needed and still holds the starting + cluster of the renamed directory - reuse it to address that + directory as the one to read from. Ensure the cluster chain is + walked from the start. */ + old_dir_entry.fx_dir_entry_last_search_cluster = 0; + + /* Read the second directory entry of the renamed directory - its + ".." entry, created together with the directory. new_dir_entry + is no longer needed either and serves as the destination. */ + i = 1; + status = _fx_directory_entry_read(media_ptr, &old_dir_entry, &i, &new_dir_entry); + + /* Update the entry only if it is the ".." entry the FAT format + places at this position - on a non-conformant media the rename + result is left as it is - and only if the stored cluster does + not match the new parent already. A rename inside one parent + ends here without an additional write. */ + if ((status == FX_SUCCESS) && + (new_dir_entry.fx_dir_entry_name[0] == '.') && + (new_dir_entry.fx_dir_entry_name[1] == '.') && + (new_dir_entry.fx_dir_entry_name[2] == 0) && + (new_dir_entry.fx_dir_entry_cluster != parent_cluster)) + { + + /* Update the stored parent cluster. Everything else of the + entry - its position included - stays untouched: the first + two entries of a directory are a format invariant other + services rely on. */ + new_dir_entry.fx_dir_entry_cluster = parent_cluster; + + /* Write the updated ".." entry back. */ + status = _fx_directory_entry_write(media_ptr, &new_dir_entry); + } + + /* Determine if the ".." update was successful. */ if (status != FX_SUCCESS) { +#ifdef FX_ENABLE_FAULT_TOLERANT FX_FAULT_TOLERANT_TRANSACTION_FAIL(media_ptr); +#endif /* FX_ENABLE_FAULT_TOLERANT */ /* Release media protection. */ FX_UNPROTECT - /* Return the bad status. */ + /* Return the error code. */ return(status); } +#ifdef FX_ENABLE_FAULT_TOLERANT /* End transaction. */ status = _fx_fault_tolerant_transaction_end(media_ptr); #endif /* FX_ENABLE_FAULT_TOLERANT */ diff --git a/test/regression_test/filex_directory_rename_test.c b/test/regression_test/filex_directory_rename_test.c index 36fd181..005379c 100644 --- a/test/regression_test/filex_directory_rename_test.c +++ b/test/regression_test/filex_directory_rename_test.c @@ -15,6 +15,7 @@ #include "tx_api.h" #endif #include "fx_api.h" +#include "fx_directory.h" #include "fx_ram_driver_test.h" #include "fx_fault_tolerant.h" #include "fx_utility.h" @@ -35,6 +36,7 @@ static TX_THREAD ftest_0; #endif static FX_MEDIA ram_disk; +static FX_FILE probe_file; static CHAR max_name[FX_MAX_LONG_NAME_LEN + 2]; static CHAR max_newname[FX_MAX_LONG_NAME_LEN + 1]; @@ -55,6 +57,9 @@ static UCHAR fault_tolerant_buffer[FAULT_TOLERANT_SIZE]; void filex_directory_rename_application_define(void *first_unused_memory); static void ftest_0_entry(ULONG thread_input); +static UINT dot_dot_probe(CHAR *marker_path); +static UINT directory_cluster_read(CHAR *directory_path, ULONG *cluster_ptr); +static UINT dot_dot_cluster_read(CHAR *directory_path, ULONG *cluster_ptr); VOID _fx_ram_driver(FX_MEDIA *media_ptr); void test_control_return(UINT status); @@ -102,6 +107,90 @@ void filex_directory_rename_application_define(void *first_unused_memory) +/* Open and close the file at marker_path - a path that contains a ".." + component - to report where ".." currently leads. */ +static UINT dot_dot_probe(CHAR *marker_path) +{ + +UINT status; + + status = fx_file_open(&ram_disk, &probe_file, marker_path, FX_OPEN_FOR_READ); + if (status == FX_SUCCESS) + { + fx_file_close(&probe_file); + } + + return(status); +} + + +/* Return the starting cluster of the directory at directory_path. */ +static UINT directory_cluster_read(CHAR *directory_path, ULONG *cluster_ptr) +{ + +UINT status; +FX_DIR_ENTRY dir_entry; + + /* The directory's own entry carries its starting cluster. */ + dir_entry.fx_dir_entry_name = ram_disk.fx_media_name_buffer + FX_MAX_LONG_NAME_LEN; + dir_entry.fx_dir_entry_short_name[0] = 0; + status = _fx_directory_search(&ram_disk, directory_path, &dir_entry, FX_NULL, FX_NULL); + + /* Return the cluster only on success. */ + if (status == FX_SUCCESS) + { + *cluster_ptr = dir_entry.fx_dir_entry_cluster; + } + + return(status); +} + + +/* Read the ".." entry - entry 1 - of the directory at directory_path and + return the parent starting cluster stored in it. Verifies that the + entry actually reads back as "..". */ +static UINT dot_dot_cluster_read(CHAR *directory_path, ULONG *cluster_ptr) +{ + +UINT status; +ULONG entry_index; +FX_DIR_ENTRY dir_entry; +FX_DIR_ENTRY dot_dot_entry; + + /* Find the directory itself first. */ + dir_entry.fx_dir_entry_name = ram_disk.fx_media_name_buffer + FX_MAX_LONG_NAME_LEN; + dir_entry.fx_dir_entry_short_name[0] = 0; + status = _fx_directory_search(&ram_disk, directory_path, &dir_entry, FX_NULL, FX_NULL); + if (status != FX_SUCCESS) + { + return(status); + } + + /* Read its second directory entry - the ".." entry. */ + dir_entry.fx_dir_entry_last_search_cluster = 0; + dot_dot_entry.fx_dir_entry_name = ram_disk.fx_media_name_buffer + FX_MAX_LONG_NAME_LEN * 2; + dot_dot_entry.fx_dir_entry_short_name[0] = 0; + entry_index = 1; + status = _fx_directory_entry_read(&ram_disk, &dir_entry, &entry_index, &dot_dot_entry); + if (status != FX_SUCCESS) + { + return(status); + } + + /* Anything other than ".." at this position is a failure. */ + if ((dot_dot_entry.fx_dir_entry_name[0] != '.') || + (dot_dot_entry.fx_dir_entry_name[1] != '.') || + (dot_dot_entry.fx_dir_entry_name[2] != 0)) + { + return(FX_INVALID_STATE); + } + + *cluster_ptr = dot_dot_entry.fx_dir_entry_cluster; + + return(FX_SUCCESS); +} + + /* Define the test threads. */ static void ftest_0_entry(ULONG thread_input) @@ -109,6 +198,11 @@ static void ftest_0_entry(ULONG thread_input) UINT status; UINT i; + ULONG parent_cluster; + ULONG dot_dot_cluster; +#ifndef FX_MEDIA_STATISTICS_DISABLE + ULONG entry_writes_before; +#endif FX_PARAMETER_NOT_USED(thread_input); @@ -296,6 +390,137 @@ static void ftest_0_entry(ULONG thread_input) status = fx_media_close(&ram_disk); return_if_fail( status == FX_SUCCESS); + /* Format the media again: the checks below build their own fixture + and must not disturb - or depend on - the directory layout the + error-injection steps above are calibrated against. */ + status = fx_media_format(&ram_disk, + _fx_ram_driver, // Driver entry + ram_disk_memory, // RAM disk memory pointer + cache_buffer, // Media buffer pointer + CACHE_SIZE, // Media buffer size + "MY_RAM_DISK", // Volume Name + 1, // Number of FATs + 128, // Directory Entries + 0, // Hidden sectors + +#ifdef FX_ENABLE_FAULT_TOLERANT + 4096 * 8, // Total sectors + 256, // Sector size + 8, // Sectors per cluster +#else + 4096, // Total sectors + 128, // Sector size + 1, // Sectors per cluster +#endif + + 1, // Heads + 1); // Sectors per track + status += fx_media_open(&ram_disk, "RAM DISK", _fx_ram_driver, ram_disk_memory, cache_buffer, CACHE_SIZE); + return_if_fail( status == FX_SUCCESS); + +#ifdef FX_ENABLE_FAULT_TOLERANT + status = fx_fault_tolerant_enable(&ram_disk, fault_tolerant_buffer, FAULT_TOLERANT_SIZE); + return_if_fail( status == FX_SUCCESS); +#endif + + /* Moving a directory to a different parent has to update the ".." + entry inside the moved directory: it stores the starting cluster + of the parent directory - cluster 0 when the parent is the root + directory. Exercise every parent transition and verify both the + path resolution through ".." and the on-media entry itself. */ + status = fx_directory_create(&ram_disk, "/DOTA"); + status += fx_directory_create(&ram_disk, "/DOTB"); + status += fx_file_create(&ram_disk, "/MARKR.TXT"); + status += fx_file_create(&ram_disk, "/DOTA/MARKA.TXT"); + status += fx_file_create(&ram_disk, "/DOTB/MARKB.TXT"); + status += fx_directory_create(&ram_disk, "/DOTA/SUB"); + return_if_fail( status == FX_SUCCESS); + + /* The probe itself has to work before anything is moved: ".." of a + new directory leads to the parent it was created in. */ + status = dot_dot_probe("/DOTA/SUB/../MARKA.TXT"); + return_if_fail( status == FX_SUCCESS); + status = dot_dot_probe("/DOTA/SUB/../MARKB.TXT"); + return_if_fail( status == FX_NOT_FOUND); + + /* Move the directory to a different parent directory. Exactly one + directory entry write - the ".." update - is added to the two + parent entry writes. */ +#ifndef FX_MEDIA_STATISTICS_DISABLE + entry_writes_before = ram_disk.fx_media_directory_entry_writes; +#endif + status = fx_directory_rename(&ram_disk, "/DOTA/SUB", "/DOTB/SUB"); + return_if_fail( status == FX_SUCCESS); +#ifndef FX_MEDIA_STATISTICS_DISABLE + return_if_fail( (ram_disk.fx_media_directory_entry_writes - entry_writes_before) == 3); +#endif + + /* ".." now leads to the new parent and only to the new parent. */ + status = dot_dot_probe("/DOTB/SUB/../MARKB.TXT"); + return_if_fail( status == FX_SUCCESS); + status = dot_dot_probe("/DOTB/SUB/../MARKA.TXT"); + return_if_fail( status == FX_NOT_FOUND); + + /* The on-media ".." entry stores the new parent's starting cluster. */ + status = directory_cluster_read("/DOTB", &parent_cluster); + return_if_fail( status == FX_SUCCESS); + status = dot_dot_cluster_read("/DOTB/SUB", &dot_dot_cluster); + return_if_fail( status == FX_SUCCESS); + return_if_fail( (dot_dot_cluster == parent_cluster) && (parent_cluster != 0)); + + /* Move the directory into the root directory: ".." stores cluster 0 + there, per the FAT convention directory create itself writes. */ + status = fx_directory_rename(&ram_disk, "/DOTB/SUB", "/SUB"); + return_if_fail( status == FX_SUCCESS); + status = dot_dot_probe("/SUB/../MARKR.TXT"); + return_if_fail( status == FX_SUCCESS); + status = dot_dot_probe("/SUB/../MARKB.TXT"); + return_if_fail( status == FX_NOT_FOUND); + status = dot_dot_cluster_read("/SUB", &dot_dot_cluster); + return_if_fail( (status == FX_SUCCESS) && (dot_dot_cluster == 0)); + + /* Move the directory back out of the root directory: 0 becomes the + new parent's cluster again. */ + status = fx_directory_rename(&ram_disk, "/SUB", "/DOTA/SUB"); + return_if_fail( status == FX_SUCCESS); + status = dot_dot_probe("/DOTA/SUB/../MARKA.TXT"); + return_if_fail( status == FX_SUCCESS); + status = dot_dot_probe("/DOTA/SUB/../MARKR.TXT"); + return_if_fail( status == FX_NOT_FOUND); + status = directory_cluster_read("/DOTA", &parent_cluster); + return_if_fail( status == FX_SUCCESS); + status = dot_dot_cluster_read("/DOTA/SUB", &dot_dot_cluster); + return_if_fail( (status == FX_SUCCESS) && (dot_dot_cluster == parent_cluster) && (parent_cluster != 0)); + + /* A rename inside one parent does not touch ".." - and adds no third + directory entry write. */ + status = fx_directory_create(&ram_disk, "/DOTC"); + return_if_fail( status == FX_SUCCESS); +#ifndef FX_MEDIA_STATISTICS_DISABLE + entry_writes_before = ram_disk.fx_media_directory_entry_writes; +#endif + status = fx_directory_rename(&ram_disk, "/DOTC", "/DOTD"); + return_if_fail( status == FX_SUCCESS); +#ifndef FX_MEDIA_STATISTICS_DISABLE + return_if_fail( (ram_disk.fx_media_directory_entry_writes - entry_writes_before) == 2); +#endif + status = dot_dot_probe("/DOTD/../MARKR.TXT"); + return_if_fail( status == FX_SUCCESS); + status = dot_dot_cluster_read("/DOTD", &dot_dot_cluster); + return_if_fail( (status == FX_SUCCESS) && (dot_dot_cluster == 0)); + + /* Same inside a sub-directory: ".." keeps the parent's cluster. */ + status = fx_directory_rename(&ram_disk, "/DOTA/SUB", "/DOTA/SUB2"); + return_if_fail( status == FX_SUCCESS); + status = dot_dot_probe("/DOTA/SUB2/../MARKA.TXT"); + return_if_fail( status == FX_SUCCESS); + status = dot_dot_cluster_read("/DOTA/SUB2", &dot_dot_cluster); + return_if_fail( (status == FX_SUCCESS) && (dot_dot_cluster == parent_cluster)); + + /* Close the media. */ + status = fx_media_close(&ram_disk); + return_if_fail( status == FX_SUCCESS); + printf("SUCCESS!\n"); test_control_return(0); }