diff --git a/.gitignore b/.gitignore index f026ba2d..86ee854d 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,7 @@ config.h *.valgrind .vscode tests/tmp/* +tests/tmp_?????? .DS_Store config.guess~ diff --git a/lib/block.c b/lib/block.c index 89ba292b..89454631 100644 --- a/lib/block.c +++ b/lib/block.c @@ -202,3 +202,78 @@ bool LCH_BlockGetTimestamp(const LCH_Json *const block, } return true; } + +static bool IsValidBlockId(const char *const block_id) { + assert(block_id != NULL); + + size_t i; + for (i = 0; block_id[i] != '\0'; i++) { + if (!(block_id[i] >= '0' && block_id[i] <= '9') && + !(block_id[i] >= 'a' && block_id[i] <= 'f')) { + /* Character is not in range [0-9] or [a-f] */ + return false; + } + } + + /* Make sure block ID is 40 characters long */ + const bool correct_length = (i == strlen(LCH_GENISIS_BLOCK_ID)); + return correct_length; +} + +char *LCH_BlockIdFromArgument(const char *const work_dir, + const char *const argument) { + assert(work_dir != NULL); + assert(argument != NULL); + + char path[PATH_MAX]; + if (!LCH_FilePathJoin(path, PATH_MAX, 2, work_dir, "blocks")) { + /* Error already logged */ + return NULL; + } + + size_t index, num_matching = 0; + + LCH_List *const blocks = LCH_FileListDirectory(path, true); + + /* Add genesis block to the list */ + char *const genisis_id = LCH_StringDuplicate(LCH_GENISIS_BLOCK_ID); + if (genisis_id == NULL) { + LCH_ListDestroy(blocks); + return NULL; + } + + if (!LCH_ListAppend(blocks, genisis_id, free)) { + /* Error already logged */ + free(genisis_id); + LCH_ListDestroy(blocks); + return NULL; + } + + const size_t num_blocks = LCH_ListLength(blocks); + + for (size_t i = 0; i < num_blocks; i++) { + const char *const filename = (char *)LCH_ListGet(blocks, i); + if (!IsValidBlockId(filename)) { + LCH_LOG_WARNING( + "The file '%s%c%s' does not conform with the block naming convention " + "and will be ignored", + path, LCH_PATH_SEP, filename); + } else if (LCH_StringStartsWith(filename, argument)) { + index = i; + num_matching += 1; + } + } + + char *block_id = NULL; + if (num_matching != 1) { + LCH_LOG_ERROR("%s block identifier '%s': %zu blocks found", + (num_matching > 1) ? "Ambiguous" : "Unknown", argument, + num_matching); + } else { + const char *const filename = (char *)LCH_ListGet(blocks, index); + block_id = LCH_StringDuplicate(filename); + } + + LCH_ListDestroy(blocks); + return block_id; +} diff --git a/lib/block.h b/lib/block.h index 7ec387ee..316aa0fd 100644 --- a/lib/block.h +++ b/lib/block.h @@ -16,4 +16,14 @@ const LCH_Json *LCH_BlockGetPayload(const LCH_Json *block); LCH_Json *LCH_BlockRemovePayload(const LCH_Json *block); bool LCH_BlockGetTimestamp(const LCH_Json *block, double *timestamp); +/** + * @brief Get block ID from partial hash + * @param work_dir Leech work directory + * @param argument Argument containing partial hash matching the start of the + * identifier of an existing block + * @return Full block identifier or NULL in case of ambiguous/unknown blocks or + * memory error + */ +char *LCH_BlockIdFromArgument(const char *work_dir, const char *argument); + #endif // _LEECH_BLOCK_H diff --git a/lib/files.c b/lib/files.c index 4a4a3922..5550b735 100644 --- a/lib/files.c +++ b/lib/files.c @@ -44,9 +44,8 @@ bool LCH_FileSize(FILE *file, size_t *size) { /******************************************************************************/ bool LCH_FileExists(const char *const path) { - struct stat sb; - memset(&sb, 0, sizeof(struct stat)); - return stat(path, &sb) == 0; + const bool exists = LCH_FileIsRegular(path) || LCH_FileIsDirectory(path); + return exists; } /******************************************************************************/ @@ -54,7 +53,9 @@ bool LCH_FileExists(const char *const path) { bool LCH_FileIsRegular(const char *const path) { struct stat sb; memset(&sb, 0, sizeof(struct stat)); - return (stat(path, &sb) == 0) && ((sb.st_mode & S_IFMT) == S_IFREG); + const bool is_regular = + (lstat(path, &sb) == 0) && ((sb.st_mode & S_IFMT) == S_IFREG); + return is_regular; } /******************************************************************************/ @@ -62,7 +63,9 @@ bool LCH_FileIsRegular(const char *const path) { bool LCH_FileIsDirectory(const char *const path) { struct stat sb; memset(&sb, 0, sizeof(struct stat)); - return (stat(path, &sb) == 0) && ((sb.st_mode & S_IFMT) == S_IFDIR); + const bool is_directory = + (lstat(path, &sb) == 0) && ((sb.st_mode & S_IFMT) == S_IFDIR); + return is_directory; } /******************************************************************************/ @@ -108,11 +111,62 @@ bool LCH_FilePathJoin(char *path, const size_t path_max, const size_t n_items, /******************************************************************************/ -bool LCH_FileDelete(const char *const filename) { - if (unlink(filename) != 0) { - LCH_LOG_ERROR("Failed to delete file '%s': %s", filename, strerror(errno)); +bool LCH_FileDelete(const char *const parent) { + assert(parent != NULL); + + if (LCH_FileIsDirectory(parent)) { + LCH_List *const children = LCH_FileListDirectory(parent, false); + if (children == NULL) { + return false; + } + + char path[PATH_MAX]; + const size_t num_children = LCH_ListLength(children); + + for (size_t i = 0; i < num_children; i++) { + const char *const child = (char *)LCH_ListGet(children, i); + assert(child != NULL); + + if (LCH_StringEqual(child, ".") || LCH_StringEqual(child, "..")) { + continue; + } + + if (!LCH_FilePathJoin(path, sizeof(path), 2, parent, child)) { + /* Error is already logged */ + LCH_ListDestroy(children); + return false; + } + + if (!LCH_FileDelete(path)) { + /* Error is already logged */ + LCH_ListDestroy(children); + return false; + } + } + + LCH_ListDestroy(children); + + const int ret = rmdir(parent); + if (ret == -1) { + LCH_LOG_ERROR("Failed to remove directory '%s': %s", parent, + strerror(errno)); + return false; + } + LCH_LOG_DEBUG("Removed directory '%s'", parent); + } else if (LCH_FileIsRegular(parent)) { + const int ret = unlink(parent); + if (ret == -1) { + LCH_LOG_ERROR("Failed to delete regular file '%s': %s", parent, + strerror(errno)); + return false; + } + LCH_LOG_DEBUG("Deleted regular file '%s'", parent); + } else { + LCH_LOG_ERROR( + "Failed to delete file '%s': It's not a directory or regular file"); return false; } + return true; } @@ -126,7 +180,7 @@ bool LCH_FileCreateParentDirectories(const char *const filename) { LCH_List *const dirs = LCH_ListCreate(); struct stat sb; - while (stat(parent, &sb) == -1) { + while (lstat(parent, &sb) == -1) { char *const dir = LCH_StringDuplicate(parent); if (dir == NULL) { LCH_ListDestroy(dirs); diff --git a/lib/leech.c b/lib/leech.c index e526a3e2..f7431603 100644 --- a/lib/leech.c +++ b/lib/leech.c @@ -478,13 +478,19 @@ static LCH_Json *MergeBlocks(const LCH_Instance *const instance, return merged; } -LCH_Buffer *LCH_Diff(const char *const work_dir, const char *const final_id) { +LCH_Buffer *LCH_Diff(const char *const work_dir, const char *const argument) { assert(work_dir != NULL); - assert(final_id != NULL); + assert(argument != NULL); + + char *const final_id = LCH_BlockIdFromArgument(work_dir, argument); + if (final_id == NULL) { + return NULL; + } LCH_Instance *const instance = LCH_InstanceLoad(work_dir); if (instance == NULL) { LCH_LOG_ERROR("Failed to load instance from configuration file"); + free(final_id); return NULL; } @@ -496,6 +502,7 @@ LCH_Buffer *LCH_Diff(const char *const work_dir, const char *const final_id) { "Failed to get block identifier from the head of the chain. " "Maybe there has not been any commits yet?"); LCH_InstanceDestroy(instance); + free(final_id); return NULL; } @@ -504,6 +511,7 @@ LCH_Buffer *LCH_Diff(const char *const work_dir, const char *const final_id) { LCH_LOG_ERROR("Failed to create patch"); free(block_id); LCH_InstanceDestroy(instance); + free(final_id); return NULL; } @@ -513,6 +521,7 @@ LCH_Buffer *LCH_Diff(const char *const work_dir, const char *const final_id) { LCH_LOG_ERROR("Failed to create empty block"); LCH_JsonDestroy(patch); LCH_InstanceDestroy(instance); + free(final_id); return NULL; } @@ -521,9 +530,11 @@ LCH_Buffer *LCH_Diff(const char *const work_dir, const char *const final_id) { LCH_LOG_ERROR("Failed to generate patch file"); LCH_JsonDestroy(patch); LCH_InstanceDestroy(instance); + free(final_id); return NULL; } LCH_InstanceDestroy(instance); + free(final_id); if (!LCH_PatchAppendBlock(patch, block)) { LCH_LOG_ERROR("Failed to append block to patch"); diff --git a/tests/acceptance.py b/tests/acceptance.py index b3d401eb..4bb05ed1 100644 --- a/tests/acceptance.py +++ b/tests/acceptance.py @@ -7,9 +7,6 @@ import os import json -# If you want to run this as current user -# sudo -u postgres createuser -s larsewi - SEED = 1 # Seed used by random generator CHANCE = 20 # Percent chance of report collection HUB_ID = "SHA=b9353fd" @@ -168,7 +165,7 @@ def record_state(self): execute(cmd) def generate_diff(self): - lastseen = "0000000000000000000000000000000000000000" + lastseen = "00000" path = Path(self.hub_workdir, self.id) if path.exists(): with open(path, "r") as f: diff --git a/tests/check_block.c b/tests/check_block.c index af0f5308..b36cbe64 100644 --- a/tests/check_block.c +++ b/tests/check_block.c @@ -1,6 +1,10 @@ #include +#include +#include +#include #include "../lib/block.h" +#include "../lib/files.h" #include "../lib/logger.h" START_TEST(test_LCH_BlockCreate) { @@ -42,6 +46,84 @@ START_TEST(test_LCH_BlockCreate) { } END_TEST +START_TEST(test_LCH_BlockIdFromArgument) { + char tmpl[] = "tmp_XXXXXX"; + const char *work_dir = mkdtemp(tmpl); + ck_assert_ptr_nonnull(work_dir); + + const char *blocks[] = { + "0820ee7abd43af0221f2ad3f81f667dd87cad6c8", + "0957d9468925b66a5acbdd6551c11dc6344337b3", + "be3e991161dcde612b61be9562e08942e9a47903", + "f80cc0ac9dc567acb99b076412c9884cbaa84f0", + "8de90ff4c64c0a251d0cdb45b4ec2253b7c6e2a3a", + "invalid block identifier", + "3d28755d158bf7a7aabbc308c527fdc9d413c9c8", + "3d28755d158b158bf7a7aabbc308c527fdc9d4c8", + NULL, + }; + + LCH_Buffer *const buffer = LCH_BufferCreate(); + ck_assert_ptr_nonnull(buffer); + + char filename[PATH_MAX]; + for (size_t i = 0; blocks[i] != NULL; i++) { + /* Create some empty files in $(work_dir)/blocks/ */ + ck_assert( + LCH_FilePathJoin(filename, PATH_MAX, 3, work_dir, "blocks", blocks[i])); + ck_assert(LCH_BufferWriteFile(buffer, filename)); + } + + LCH_BufferDestroy(buffer); + + char *block_id = LCH_BlockIdFromArgument(work_dir, "0820ee7"); + ck_assert_str_eq(block_id, blocks[0]); + free(block_id); + + block_id = LCH_BlockIdFromArgument(work_dir, "0957d946"); + ck_assert_str_eq(block_id, blocks[1]); + free(block_id); + + /* Try with the entire hash */ + block_id = LCH_BlockIdFromArgument( + work_dir, "be3e991161dcde612b61be9562e08942e9a47903"); + ck_assert_str_eq(block_id, blocks[2]); + free(block_id); + + /* Try with more than the entire hash */ + block_id = LCH_BlockIdFromArgument( + work_dir, "be3e991161dcde612b61be9562e08942e9a47903a"); + ck_assert_ptr_null(block_id); + free(block_id); + + block_id = LCH_BlockIdFromArgument(work_dir, "957d94"); + ck_assert_ptr_null(block_id); + free(block_id); + + block_id = LCH_BlockIdFromArgument(work_dir, "f80cc0ac9"); + ck_assert_ptr_null(block_id); + free(block_id); + + block_id = LCH_BlockIdFromArgument(work_dir, "8de90ff4c64c0"); + ck_assert_ptr_null(block_id); + free(block_id); + + block_id = LCH_BlockIdFromArgument(work_dir, "invalid"); + ck_assert_ptr_null(block_id); + free(block_id); + + block_id = LCH_BlockIdFromArgument(work_dir, "3d28755d158b"); + ck_assert_ptr_null(block_id); + free(block_id); + + block_id = LCH_BlockIdFromArgument(work_dir, "3d28755d158b1"); + ck_assert_str_eq(block_id, blocks[7]); + free(block_id); + + ck_assert(LCH_FileDelete(work_dir)); +} +END_TEST + Suite *BlockSuite(void) { Suite *s = suite_create("block.c"); { @@ -49,5 +131,10 @@ Suite *BlockSuite(void) { tcase_add_test(tc, test_LCH_BlockCreate); suite_add_tcase(s, tc); } + { + TCase *tc = tcase_create("LCH_BlockIdFromArgument"); + tcase_add_test(tc, test_LCH_BlockIdFromArgument); + suite_add_tcase(s, tc); + } return s; }