diff --git a/README.md b/README.md index fa3cef6..1adae88 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ $ ./fkvs-cli -h 127.0.0.1 -p 5995 --non-interactive |---|---|---| | `PING` | `PING` or `PING value` | Test connectivity; returns `PONG` or echoes the value | | `INFO` | `INFO` | Display server statistics (uptime, memory, connected clients) | +| `KEYS` | `KEYS` | List all non-expired stored keys | ## Documentation diff --git a/src/commands/client/client_command_handlers.c b/src/commands/client/client_command_handlers.c index fed93b7..a97fab8 100644 --- a/src/commands/client/client_command_handlers.c +++ b/src/commands/client/client_command_handlers.c @@ -7,7 +7,9 @@ #include "../../utils.h" #include +#include #include +#include #include #include #include @@ -17,6 +19,22 @@ #define MAX_KEY_LEN 512 #define MAX_VALUE_LEN 512 +static bool command_equals(const char *input, const char *expected) +{ + while (isspace((unsigned char)*input)) + input++; + + size_t input_len = strlen(input); + while (input_len > 0 && + isspace((unsigned char)input[input_len - 1])) { + input_len--; + } + + const size_t expected_len = strlen(expected); + return input_len == expected_len && + strncasecmp(input, expected, expected_len) == 0; +} + void cmd_get(const command_args_t args, void (*response_cb)(client_t *client)) { if (strncasecmp(args.cmd, "GET ", 4) != 0) { @@ -386,6 +404,27 @@ void cmd_persist(const command_args_t args, void (*response_cb)(client_t *client response_cb(args.client); } +void cmd_keys(const command_args_t args, void (*response_cb)(client_t *client)) +{ + if (!command_equals(args.cmd, "KEYS")) { + return; + } + + size_t cmd_len; + unsigned char *keys_cmd = construct_keys_command(&cmd_len); + if (!keys_cmd) { + fprintf(stderr, "Failed to construct KEYS command\n"); + return; + } + + assert(cmd_len > 0); + assert(args.client->fd > 0); + + send(args.client->fd, keys_cmd, cmd_len, 0); + free(keys_cmd); + response_cb(args.client); +} + /* * TODO: This approach works but is cumbersome to maintain. For future * reference, lets implement a solution that doesn't require us to have a @@ -408,7 +447,8 @@ void cmd_unknown(const command_args_t args, strncmp(args.cmd, "EXPIRE ", 7) && strncmp(args.cmd, "TTL ", 4) && strncmp(args.cmd, "PERSIST ", 8) && - strncmp(args.cmd, "INFO", 5)) { + strncmp(args.cmd, "INFO", 5) && + !command_equals(args.cmd, "KEYS")) { printf("Unknown command \n"); } } @@ -421,7 +461,8 @@ const cmd_t command_table[] = { {"cmd_del", cmd_del}, {"cmd_expire", cmd_expire}, {"cmd_ttl", cmd_ttl}, {"cmd_persist", cmd_persist}, {"cmd_unknown", cmd_unknown}, - {"cmd_info", cmd_info}}; + {"cmd_info", cmd_info}, + {"cmd_keys", cmd_keys}}; void execute_command(const char *cmd, client_t *client, void (*response_cb)(client_t *client)) @@ -585,6 +626,21 @@ void command_response_handler(client_t *client) printf("Memory allocation failed\n"); } + } else if (client->buffer[2] == CMD_KEYS) { + const size_t value_len = + client->buffer[3] << 8 | client->buffer[4]; + if (value_len == 0) { + printf("(empty list)\n"); + } else { + char *data = malloc(value_len + 1); + if (data) { + memcpy(data, &client->buffer[5], value_len); + data[value_len] = '\0'; + printf("%s\n", data); + free(data); + } + } + } else if (client->buffer[2] == CMD_PING) { const size_t value_len = client->buffer[3] << 8 | client->buffer[4]; diff --git a/src/commands/client/client_command_handlers.h b/src/commands/client/client_command_handlers.h index 3e3fbf4..ad4c951 100644 --- a/src/commands/client/client_command_handlers.h +++ b/src/commands/client/client_command_handlers.h @@ -54,6 +54,8 @@ void cmd_ttl(command_args_t args, void (*response_cb)(client_t *client)); void cmd_persist(command_args_t args, void (*response_cb)(client_t *client)); +void cmd_keys(command_args_t args, void (*response_cb)(client_t *client)); + void command_response_handler(client_t *client); #endif // CLIENT_COMMAND_HANDLERS diff --git a/src/commands/common/command_defs.h b/src/commands/common/command_defs.h index 7661b06..43ba63e 100644 --- a/src/commands/common/command_defs.h +++ b/src/commands/common/command_defs.h @@ -13,5 +13,6 @@ #define CMD_EXPIRE 0x0A #define CMD_TTL 0x0B #define CMD_PERSIST 0x0C +#define CMD_KEYS 0x0D #endif // COMMAND_DEFS_H diff --git a/src/commands/common/command_parser.c b/src/commands/common/command_parser.c index 807566d..ef6b7e1 100644 --- a/src/commands/common/command_parser.c +++ b/src/commands/common/command_parser.c @@ -327,3 +327,21 @@ unsigned char *construct_persist_command(const char *key, size_t *command_len) return binary_cmd; } + +unsigned char *construct_keys_command(size_t *command_len) +{ + const size_t core_cmd_len = 1; + *command_len = 2 + core_cmd_len; + + unsigned char *binary_cmd = malloc(*command_len); + if (!binary_cmd) { + return NULL; + } + + binary_cmd[0] = core_cmd_len >> 8 & 0xFF; + binary_cmd[1] = core_cmd_len & 0xFF; + + binary_cmd[2] = CMD_KEYS; + + return binary_cmd; +} diff --git a/src/commands/common/command_parser.h b/src/commands/common/command_parser.h index 9f46703..369a06d 100644 --- a/src/commands/common/command_parser.h +++ b/src/commands/common/command_parser.h @@ -37,4 +37,6 @@ unsigned char *construct_ttl_command(const char *key, size_t *command_len); unsigned char *construct_persist_command(const char *key, size_t *command_len); +unsigned char *construct_keys_command(size_t *command_len); + #endif // COMMAND_PARSER_H diff --git a/src/commands/common/command_registry.c b/src/commands/common/command_registry.c index a612e9f..359ff95 100644 --- a/src/commands/common/command_registry.c +++ b/src/commands/common/command_registry.c @@ -192,6 +192,34 @@ void send_reply(client_t *client, const unsigned char *buffer, wbuf_append(client, buffer, bytes_read); } +void send_keys_reply(client_t *client, const unsigned char *data, + size_t data_len) +{ + if (client->fd < 0) + return; + + const size_t core_cmd_len = 1 + 2 + data_len; + const size_t full_frame_length = 2 + core_cmd_len; + + if (core_cmd_len > UINT16_MAX) { + send_error(client); + return; + } + + if (!wbuf_reserve(client, full_frame_length)) + return; + + const unsigned char header[] = { + (core_cmd_len >> 8) & 0xFF, + core_cmd_len & 0xFF, + CMD_KEYS, + (data_len >> 8) & 0xFF, + data_len & 0xFF, + }; + wbuf_append(client, header, sizeof(header)); + wbuf_append(client, data, data_len); +} + void send_pong(client_t *client, const unsigned char *buffer, size_t bytes_read) { diff --git a/src/commands/common/command_registry.h b/src/commands/common/command_registry.h index 562d785..78cfd71 100644 --- a/src/commands/common/command_registry.h +++ b/src/commands/common/command_registry.h @@ -17,6 +17,8 @@ void wbuf_flush(client_t *client); void send_ok(client_t *client); void send_error(client_t *client); void send_reply(client_t *client, const unsigned char *buffer, size_t bytes_read); +void send_keys_reply(client_t *client, const unsigned char *data, + size_t data_len); void send_pong(client_t *client, const unsigned char *buffer, size_t bytes_read); #endif // COMMAND_REGISTRY_H diff --git a/src/commands/server/server_command_handlers.c b/src/commands/server/server_command_handlers.c index b38e968..d0d9e85 100644 --- a/src/commands/server/server_command_handlers.c +++ b/src/commands/server/server_command_handlers.c @@ -19,8 +19,8 @@ static hashtable_t *expires = NULL; static bool check_and_expire(const unsigned char *key, size_t key_len) { if (is_expired(expires, key, key_len)) { - delete_value(table, key, key_len); delete_value(expires, key, key_len); + delete_value(table, key, key_len); return true; } return false; @@ -42,6 +42,7 @@ void init_command_handlers(db_t *db) register_command(CMD_EXPIRE, handle_expire_command); register_command(CMD_TTL, handle_ttl_command); register_command(CMD_PERSIST, handle_persist_command); + register_command(CMD_KEYS, handle_keys_command); } void handle_set_command(client_t *client, unsigned char *buffer, size_t bytes_read) @@ -964,3 +965,98 @@ void handle_persist_command(client_t *client, unsigned char *buffer, send_ok(client); } + +void handle_keys_command(client_t *client, unsigned char *buffer, + size_t bytes_read) +{ + if (bytes_read != 3) { + send_error(client); + return; + } + + const uint16_t core_len = ((uint16_t)buffer[0] << 8) | buffer[1]; + if (core_len != 1 || buffer[2] != CMD_KEYS) { + send_error(client); + return; + } + + const size_t max_output = 65500; + size_t capacity = 4096; + if (capacity > max_output) { + capacity = max_output; + } + + char *buf = malloc(capacity); + if (!buf) { + send_error(client); + return; + } + + size_t used = 0; + size_t count = 0; + + for (size_t i = 0; i < table->size; i++) { + hash_table_entry_t *entry = table->buckets[i]; + while (entry) { + hash_table_entry_t *next = entry->next; + + if (check_and_expire(entry->key, entry->key_len)) { + entry = next; + continue; + } + + // Format: "N) key\n" + char num_buf[24]; + int num_len = snprintf(num_buf, sizeof(num_buf), "%zu) ", count + 1); + if (num_len < 0 || (size_t)num_len >= sizeof(num_buf)) { + free(buf); + send_error(client); + return; + } + + size_t line_len = (size_t)num_len + entry->key_len + 1; // +1 for \n + + if (line_len > max_output - used) { + free(buf); + send_error(client); + return; + } + + if (used + line_len > capacity) { + size_t new_cap = capacity * 2; + if (new_cap > max_output) { + new_cap = max_output; + } + if (new_cap < used + line_len) { + new_cap = used + line_len; + } + char *tmp = realloc(buf, new_cap); + if (!tmp) { + free(buf); + send_error(client); + return; + } + buf = tmp; + capacity = new_cap; + } + + memcpy(buf + used, num_buf, num_len); + used += num_len; + memcpy(buf + used, entry->key, entry->key_len); + used += entry->key_len; + buf[used] = '\n'; + used++; + + count++; + entry = next; + } + } + + if (count == 0) { + send_keys_reply(client, (const unsigned char *)"", 0); + } else { + send_keys_reply(client, (const unsigned char *)buf, used); + } + + free(buf); +} diff --git a/src/commands/server/server_command_handlers.h b/src/commands/server/server_command_handlers.h index 5843eac..dbacd16 100644 --- a/src/commands/server/server_command_handlers.h +++ b/src/commands/server/server_command_handlers.h @@ -43,4 +43,7 @@ void handle_ttl_command(client_t *client, unsigned char *buffer, void handle_persist_command(client_t *client, unsigned char *buffer, size_t bytes_read); +void handle_keys_command(client_t *client, unsigned char *buffer, + size_t bytes_read); + #endif // SERVER_COMMAND_HANDLERS_H diff --git a/tests/test_integration.c b/tests/test_integration.c index 68eb33d..a90730c 100644 --- a/tests/test_integration.c +++ b/tests/test_integration.c @@ -8,6 +8,7 @@ */ #include "../src/client.h" +#include "../src/commands/common/command_defs.h" #include "../src/commands/common/command_parser.h" #include "../src/commands/common/command_registry.h" #include "../src/commands/server/server_command_handlers.h" @@ -879,6 +880,174 @@ static void test_set_ex_zero(void) printf(" test_set_ex_zero passed.\n"); } +/* ── KEYS helpers ──────────────────────────────────────────────────── */ + +/** Dispatch KEYS and return raw response. */ +static ssize_t dispatch_keys(fixture_t *f, unsigned char *resp, + size_t resp_size) +{ + size_t len; + unsigned char *cmd = construct_keys_command(&len); + assert(cmd); + ssize_t r = dispatch_and_recv(f, cmd, len, resp, resp_size); + free(cmd); + return r; +} + +/** Check that a KEYS response has CMD_KEYS tag and the given value_len. */ +static bool resp_is_keys(const unsigned char *resp, ssize_t len, + size_t *out_value_len) +{ + if (len < 5) + return false; + if (resp[2] != CMD_KEYS) + return false; + *out_value_len = ((size_t)resp[3] << 8) | resp[4]; + return true; +} + +/* ── KEYS tests ────────────────────────────────────────────────────── */ + +static void test_keys_empty_store(void) +{ + fixture_t f = setup(); + + unsigned char resp[4096]; + ssize_t r = dispatch_keys(&f, resp, sizeof resp); + + size_t value_len; + assert(r > 0 && resp_is_keys(resp, r, &value_len)); + assert(value_len == 0); + + teardown(&f); + printf(" test_keys_empty_store passed.\n"); +} + +static void test_keys_returns_stored_keys(void) +{ + fixture_t f = setup(); + + assert_set(&f, "foo", "bar", "bar"); + assert_set(&f, "hello", "world", "world"); + + unsigned char resp[4096]; + ssize_t r = dispatch_keys(&f, resp, sizeof resp); + + size_t value_len; + assert(r > 0 && resp_is_keys(resp, r, &value_len)); + assert(value_len > 0); + + // Parse the response body to verify both keys are listed + char *body = malloc(value_len + 1); + assert(body); + memcpy(body, &resp[5], value_len); + body[value_len] = '\0'; + + assert(strstr(body, "foo") != NULL); + assert(strstr(body, "hello") != NULL); + + free(body); + teardown(&f); + printf(" test_keys_returns_stored_keys passed.\n"); +} + +static void test_keys_excludes_expired(void) +{ + fixture_t f = setup(); + + assert_set(&f, "persist_key", "val", "val"); + assert_set(&f, "temp_key", "val", "val"); + assert_expire_ok(&f, "temp_key", "1"); + + sleep(2); + + unsigned char resp[4096]; + ssize_t r = dispatch_keys(&f, resp, sizeof resp); + + size_t value_len; + assert(r > 0 && resp_is_keys(resp, r, &value_len)); + assert(value_len > 0); + + char *body = malloc(value_len + 1); + assert(body); + memcpy(body, &resp[5], value_len); + body[value_len] = '\0'; + + assert(strstr(body, "persist_key") != NULL); + assert(strstr(body, "temp_key") == NULL); + + free(body); + teardown(&f); + printf(" test_keys_excludes_expired passed.\n"); +} + +static void test_keys_after_del(void) +{ + fixture_t f = setup(); + + assert_set(&f, "a", "1", "1"); + assert_set(&f, "b", "2", "2"); + assert_del_ok(&f, "a"); + + unsigned char resp[4096]; + ssize_t r = dispatch_keys(&f, resp, sizeof resp); + + size_t value_len; + assert(r > 0 && resp_is_keys(resp, r, &value_len)); + assert(value_len > 0); + + char *body = malloc(value_len + 1); + assert(body); + memcpy(body, &resp[5], value_len); + body[value_len] = '\0'; + + assert(strstr(body, "b") != NULL); + assert(strstr(body, "a") == NULL); + + free(body); + teardown(&f); + printf(" test_keys_after_del passed.\n"); +} + +static void test_keys_rejects_malformed_frame(void) +{ + fixture_t f = setup(); + + unsigned char resp[4096]; + unsigned char malformed[] = {0x00, 0x02, CMD_KEYS, 0x00}; + ssize_t r = + dispatch_and_recv(&f, malformed, sizeof malformed, resp, sizeof resp); + + assert(r > 0 && resp_is_error(resp, r)); + + teardown(&f); + printf(" test_keys_rejects_malformed_frame passed.\n"); +} + +static void test_keys_rejects_truncated_output(void) +{ + fixture_t f = setup(); + + char key[641]; + + for (int i = 0; i < 110; i++) { + char prefix[16]; + int prefix_len = snprintf(prefix, sizeof(prefix), "key-%03d-", i); + assert(prefix_len > 0 && (size_t)prefix_len < sizeof(prefix)); + memset(key, 'x', sizeof(key) - 1); + memcpy(key, prefix, (size_t)prefix_len); + key[sizeof(key) - 1] = '\0'; + assert_set(&f, key, "v", "v"); + } + + unsigned char resp[4096]; + ssize_t r = dispatch_keys(&f, resp, sizeof resp); + assert(r > 0 && resp_is_error(resp, r)); + + teardown(&f); + printf(" test_keys_rejects_truncated_output passed.\n"); +} + static void test_set_ex_rejects_invalid_ttl_atomically(void) { fixture_t f = setup(); @@ -970,6 +1139,14 @@ int main(void) test_set_ex_rejects_invalid_ttl_atomically(); test_set_ex_rejects_negative_ttl_atomically(); + /* KEYS */ + test_keys_empty_store(); + test_keys_returns_stored_keys(); + test_keys_excludes_expired(); + test_keys_after_del(); + test_keys_rejects_malformed_frame(); + test_keys_rejects_truncated_output(); + printf("All integration tests passed.\n"); return 0; }