diff --git a/include/rcutils/filesystem.h b/include/rcutils/filesystem.h index 730f878c..8b802715 100644 --- a/include/rcutils/filesystem.h +++ b/include/rcutils/filesystem.h @@ -243,6 +243,54 @@ RCUTILS_PUBLIC size_t rcutils_get_file_size(const char * file_path); +/// An iterator used for enumerating directory contents +typedef struct rcutils_dir_iter_t +{ + /// The name of the enumerated file or directory + const char * entry_name; + /// The allocator used internally by iteration functions + rcutils_allocator_t allocator; + /// The platform-specific iteration state + void * state; +} rcutils_dir_iter_t; + +/// Begin iterating over the contents of the specified directory. +/* + * This function is used to list the files and directories that are contained in + * a specified directory. The structure returned by it must be deallocated using + * ::rcutils_dir_iter_end when the iteration is completed. The name of the + * enumerated entry is stored in the `entry_name` member of the returned object, + * and the first entry is already populated upon completion of this function. To + * populate the entry with the name of the next entry, use the + * ::rcutils_dir_iter_next function. Note that the "." and ".." entries are + * typically among the entries enumerated. + * \param[in] directory_path The directory path to iterate over the contents of. + * \param[in] allocator Allocator used to create the returned structure. + * \return An iterator object used to continue iterating directory contents + * \return NULL if an error occurred + */ +RCUTILS_PUBLIC +rcutils_dir_iter_t * +rcutils_dir_iter_start(const char * directory_path, const rcutils_allocator_t allocator); + +/// Continue iterating over the contents of a directory. +/* + * \param[in] iter An iterator created by ::rcutils_dir_iter_start. + * \return `True` if another entry was found + * \return `False` if there are no more entries in the directory + */ +RCUTILS_PUBLIC +bool +rcutils_dir_iter_next(rcutils_dir_iter_t * iter); + +/// Finish iterating over the contents of a directory. +/* + * \param[in] iter An iterator created by ::rcutils_dir_iter_start. + */ +RCUTILS_PUBLIC +void +rcutils_dir_iter_end(rcutils_dir_iter_t * iter); + #ifdef __cplusplus } #endif diff --git a/src/filesystem.c b/src/filesystem.c index a17b0627..0ab70773 100644 --- a/src/filesystem.c +++ b/src/filesystem.c @@ -54,6 +54,16 @@ extern "C" # define RCUTILS_PATH_DELIMITER "/" #endif // _WIN32 +typedef struct rcutils_dir_iter_state_t +{ +#ifdef _WIN32 + HANDLE handle; + WIN32_FIND_DATA data; +#else + DIR * dir; +#endif +} rcutils_dir_iter_state_t; + bool rcutils_get_cwd(char * buffer, size_t max_length) { @@ -337,6 +347,7 @@ rcutils_calculate_directory_size_with_recursion( { dir_list_t * dir_list = NULL; rcutils_ret_t ret = RCUTILS_RET_OK; + rcutils_dir_iter_t * iter = NULL; if (NULL == directory_path) { RCUTILS_SAFE_FWRITE_TO_STDERR("directory_path is NULL !"); @@ -370,36 +381,21 @@ rcutils_calculate_directory_size_with_recursion( *size = 0; -#ifdef _WIN32 - HANDLE handle = INVALID_HANDLE_VALUE; - char * dir_path = NULL; - do { - dir_path = rcutils_join_path(dir_list->path, "*", allocator); - if (NULL == dir_path) { - RCUTILS_SAFE_FWRITE_TO_STDERR("Failed to duplicate directory path !\n"); - ret = RCUTILS_RET_BAD_ALLOC; - goto fail; - } - - WIN32_FIND_DATA data; - handle = FindFirstFile(dir_path, &data); - if (INVALID_HANDLE_VALUE == handle) { - RCUTILS_SAFE_FWRITE_TO_STDERR_WITH_FORMAT_STRING( - "Can't open directory %s. Error code: %lu\n", dir_list->path, GetLastError()); + iter = rcutils_dir_iter_start(dir_list->path, allocator); + if (NULL == iter) { ret = RCUTILS_RET_ERROR; goto fail; } do { - ret = check_and_calculate_size(data.cFileName, size, max_depth, dir_list, allocator); + ret = check_and_calculate_size(iter->entry_name, size, max_depth, dir_list, allocator); if (RCUTILS_RET_OK != ret) { goto fail; } - } while (FindNextFile(handle, &data)); + } while (rcutils_dir_iter_next(iter)); - FindClose(handle); - allocator.deallocate(dir_path, allocator.state); + rcutils_dir_iter_end(iter); remove_first_dir_from_list(&dir_list, allocator); } while (dir_list); @@ -407,52 +403,124 @@ rcutils_calculate_directory_size_with_recursion( return ret; fail: - if (NULL != dir_path) { - allocator.deallocate(dir_path, allocator.state); + rcutils_dir_iter_end(iter); + free_dir_list(dir_list, allocator); + return ret; +} + +rcutils_dir_iter_t * +rcutils_dir_iter_start(const char * directory_path, const rcutils_allocator_t allocator) +{ + RCUTILS_CHECK_ARGUMENT_FOR_NULL(directory_path, NULL); + RCUTILS_CHECK_ALLOCATOR_WITH_MSG( + &allocator, "allocator is invalid", return NULL); + + rcutils_dir_iter_t * iter = (rcutils_dir_iter_t *)allocator.zero_allocate( + 1, sizeof(rcutils_dir_iter_t), allocator.state); + if (NULL == iter) { + return NULL; } + iter->allocator = allocator; - if (INVALID_HANDLE_VALUE != handle) { - FindClose(handle); + rcutils_dir_iter_state_t * state = (rcutils_dir_iter_state_t *)allocator.zero_allocate( + 1, sizeof(rcutils_dir_iter_state_t), allocator.state); + if (NULL == state) { + RCUTILS_SET_ERROR_MSG( + "Failed to allocate memory.\n"); + goto rcutils_dir_iter_start_fail; } - free_dir_list(dir_list, allocator); - return ret; -#else - DIR * dir = NULL; + iter->state = (void *)state; - struct dirent * entry; - do { - dir = opendir(dir_list->path); - if (NULL == dir) { - RCUTILS_SAFE_FWRITE_TO_STDERR_WITH_FORMAT_STRING( - "Can't open directory %s. Error code: %d\n", dir_list->path, errno); - ret = RCUTILS_RET_ERROR; - goto fail; +#ifdef _WIN32 + char * search_path = rcutils_join_path(directory_path, "*", allocator); + if (NULL == search_path) { + goto rcutils_dir_iter_start_fail; + } + state->handle = FindFirstFile(search_path, &state->data); + allocator.deallocate(search_path, allocator.state); + if (INVALID_HANDLE_VALUE == state->handle) { + DWORD error = GetLastError(); + if (ERROR_FILE_NOT_FOUND != error || !rcutils_is_directory(directory_path)) { + RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING( + "Can't open directory %s. Error code: %d\n", directory_path, error); + goto rcutils_dir_iter_start_fail; } + } else { + iter->entry_name = state->data.cFileName; + } +#else + state->dir = opendir(directory_path); + if (NULL == state->dir) { + RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING( + "Can't open directory %s. Error code: %d\n", directory_path, errno); + goto rcutils_dir_iter_start_fail; + } + + errno = 0; + struct dirent * entry = readdir(state->dir); + if (NULL != entry) { + iter->entry_name = entry->d_name; + } else if (0 != errno) { + RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING( + "Can't iterate directory %s. Error code: %d\n", directory_path, errno); + goto rcutils_dir_iter_start_fail; + } +#endif - // Scan in specified path - // If found directory, add to dir_list - // If found file, calculate file size - while (NULL != (entry = readdir(dir))) { - ret = check_and_calculate_size(entry->d_name, size, max_depth, dir_list, allocator); - if (RCUTILS_RET_OK != ret) { - goto fail; - } - } + return iter; - closedir(dir); +rcutils_dir_iter_start_fail: + rcutils_dir_iter_end(iter); + return NULL; +} - remove_first_dir_from_list(&dir_list, allocator); - } while (dir_list); +bool +rcutils_dir_iter_next(rcutils_dir_iter_t * iter) +{ + RCUTILS_CHECK_ARGUMENT_FOR_NULL(iter, false); + rcutils_dir_iter_state_t * state = (rcutils_dir_iter_state_t *)iter->state; + RCUTILS_CHECK_FOR_NULL_WITH_MSG(state, "iter is invalid", false); - return ret; +#ifdef _WIN32 + if (FindNextFile(state->handle, &state->data)) { + iter->entry_name = state->data.cFileName; + return true; + } + FindClose(state->handle); +#else + struct dirent * entry = readdir(state->dir); + if (NULL != entry) { + iter->entry_name = entry->d_name; + return true; + } +#endif -fail: - if (NULL != dir) { - closedir(dir); + iter->entry_name = NULL; + return false; +} + +void +rcutils_dir_iter_end(rcutils_dir_iter_t * iter) +{ + if (NULL == iter) { + return; } - free_dir_list(dir_list, allocator); - return ret; + + rcutils_allocator_t allocator = iter->allocator; + rcutils_dir_iter_state_t * state = (rcutils_dir_iter_state_t *)iter->state; + if (NULL != state) { +#ifdef _WIN32 + FindClose(state->handle); +#else + if (NULL != state->dir) { + closedir(state->dir); + } #endif + + allocator.deallocate(state, allocator.state); + } + + allocator.deallocate(iter, allocator.state); } size_t diff --git a/test/test_filesystem.cpp b/test/test_filesystem.cpp index cfd105a7..e263128a 100644 --- a/test/test_filesystem.cpp +++ b/test/test_filesystem.cpp @@ -13,8 +13,10 @@ // limitations under the License. #include +#include #include +#include "rcutils/error_handling.h" #include "rcutils/filesystem.h" #include "rcutils/get_env.h" @@ -456,6 +458,7 @@ TEST_F(TestFilesystemFixture, calculate_directory_size) { fs.exhaust_file_descriptors(); ret = rcutils_calculate_directory_size(path, &size, g_allocator); EXPECT_EQ(RCUTILS_RET_ERROR, ret); + rcutils_reset_error(); } } @@ -513,6 +516,7 @@ TEST_F(TestFilesystemFixture, calculate_directory_size_with_recursion) { fs.exhaust_file_descriptors(); ret = rcutils_calculate_directory_size_with_recursion(path, 0, &size, g_allocator); EXPECT_EQ(RCUTILS_RET_ERROR, ret); + rcutils_reset_error(); } } @@ -541,3 +545,59 @@ TEST_F(TestFilesystemFixture, calculate_file_size) { g_allocator.deallocate(non_existing_path, g_allocator.state); }); } + +TEST_F(TestFilesystemFixture, directory_iterator) { + char * path = + rcutils_join_path(this->test_path, "dummy_folder", g_allocator); + OSRF_TESTING_TOOLS_CPP_SCOPE_EXIT( + { + g_allocator.deallocate(path, g_allocator.state); + }); + + std::set expected { + ".", + "..", + "dummy.dummy", + }; + + rcutils_dir_iter_t * iter = rcutils_dir_iter_start(path, g_allocator); + ASSERT_NE(nullptr, iter); + OSRF_TESTING_TOOLS_CPP_SCOPE_EXIT( + { + rcutils_dir_iter_end(iter); + }); + + do { + if (1u != expected.erase(iter->entry_name)) { + ADD_FAILURE() << "Unexpected entry '" << iter->entry_name << "' was enumerated"; + } + } while (rcutils_dir_iter_next(iter)); + + for (std::string missing : expected) { + ADD_FAILURE() << "Expected entry '" << missing << "' was not enumerated"; + } +} + +TEST_F(TestFilesystemFixture, directory_iterator_non_existing) { + char * path = + rcutils_join_path(this->test_path, "non_existing_folder", g_allocator); + OSRF_TESTING_TOOLS_CPP_SCOPE_EXIT( + { + g_allocator.deallocate(path, g_allocator.state); + }); + + EXPECT_EQ(nullptr, rcutils_dir_iter_start(path, g_allocator)); + rcutils_reset_error(); +} + +TEST_F(TestFilesystemFixture, directory_iterator_on_file) { + char * path = + rcutils_join_path(this->test_path, "dummy_readable_file.txt", g_allocator); + OSRF_TESTING_TOOLS_CPP_SCOPE_EXIT( + { + g_allocator.deallocate(path, g_allocator.state); + }); + + EXPECT_EQ(nullptr, rcutils_dir_iter_start(path, g_allocator)); + rcutils_reset_error(); +}