From cfa63490e7357862c55bba2f54480e087f4b8fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lio=20Garcia?= Date: Sun, 1 Mar 2026 22:43:48 +0100 Subject: [PATCH 1/2] feat: implement KEYS command to list all stored keys Add KEYS command that returns all non-expired keys in numbered format. Uses dedicated send_keys_reply() with CMD_KEYS tag in the response frame so the client can distinguish KEYS responses (empty list vs key listing) from generic replies. Server-side handler iterates the hash table with safe next-pointer caching and lazy expiration, using a dynamic buffer with realloc growth up to the 65500-byte frame limit. Co-Authored-By: Claude Opus 4.6 --- README.md | 1 + src/commands/client/client_command_handlers.c | 42 +++++- src/commands/client/client_command_handlers.h | 2 + src/commands/common/command_defs.h | 1 + src/commands/common/command_parser.c | 18 +++ src/commands/common/command_parser.h | 2 + src/commands/common/command_registry.c | 20 +++ src/commands/common/command_registry.h | 2 + src/commands/server/server_command_handlers.c | 81 +++++++++++ src/commands/server/server_command_handlers.h | 3 + tests/test_integration.c | 135 ++++++++++++++++++ 11 files changed, 305 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fa3cef6..98cc26e 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 stored keys | ## Documentation diff --git a/src/commands/client/client_command_handlers.c b/src/commands/client/client_command_handlers.c index 4010390..0b76467 100644 --- a/src/commands/client/client_command_handlers.c +++ b/src/commands/client/client_command_handlers.c @@ -385,6 +385,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 (strncasecmp(args.cmd, "KEYS", 4) != 0) { + 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 @@ -405,7 +426,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) && + strncmp(args.cmd, "KEYS", 4)) { printf("Unknown command \n"); } } @@ -418,7 +440,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)) @@ -582,6 +605,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 55158f8..c32286c 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 \ No newline at end of file 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 dac0222..97b5c83 100644 --- a/src/commands/common/command_registry.c +++ b/src/commands/common/command_registry.c @@ -117,6 +117,26 @@ void send_reply(client_t *client, const unsigned char *buffer, wbuf_append(client, frame, full_frame_length); } +void send_keys_reply(client_t *client, const unsigned char *data, + size_t data_len) +{ + const size_t core_cmd_len = 1 + 2 + data_len; + const size_t full_frame_length = 2 + core_cmd_len; + + unsigned char frame[65536]; + assert(full_frame_length <= sizeof(frame)); + + frame[0] = (core_cmd_len >> 8) & 0xFF; + frame[1] = core_cmd_len & 0xFF; + frame[2] = CMD_KEYS; + frame[3] = (data_len >> 8) & 0xFF; + frame[4] = data_len & 0xFF; + memcpy(&frame[5], data, data_len); + + assert(client->fd > 0); + wbuf_append(client, frame, full_frame_length); +} + void send_pong(client_t *client, const unsigned char *buffer) { const size_t value_len = buffer[3] << 8 | buffer[4]; diff --git a/src/commands/common/command_registry.h b/src/commands/common/command_registry.h index acc1052..a8142e2 100644 --- a/src/commands/common/command_registry.h +++ b/src/commands/common/command_registry.h @@ -18,5 +18,7 @@ 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_pong(client_t *client, const unsigned char *buffer); +void send_keys_reply(client_t *client, const unsigned char *data, + size_t data_len); #endif // COMMAND_REGISTRY_H diff --git a/src/commands/server/server_command_handlers.c b/src/commands/server/server_command_handlers.c index 9849b71..142dc5f 100644 --- a/src/commands/server/server_command_handlers.c +++ b/src/commands/server/server_command_handlers.c @@ -41,6 +41,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) @@ -764,3 +765,83 @@ 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) +{ + assert(buffer[2] == CMD_KEYS); + + 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; + bool truncated = false; + + for (size_t i = 0; i < table->size && !truncated; 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); + + size_t line_len = (size_t)num_len + entry->key_len + 1; // +1 for \n + + if (used + line_len > max_output) { + truncated = true; + break; + } + + 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) { + truncated = true; + break; + } + 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 dd963e6..27b7cc1 100644 --- a/tests/test_integration.c +++ b/tests/test_integration.c @@ -852,6 +852,135 @@ 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"); +} + /* ── main ──────────────────────────────────────────────────────────── */ int main(void) @@ -918,6 +1047,12 @@ int main(void) test_set_ex_overwritten_by_set(); test_set_ex_zero(); + /* KEYS */ + test_keys_empty_store(); + test_keys_returns_stored_keys(); + test_keys_excludes_expired(); + test_keys_after_del(); + printf("All integration tests passed.\n"); return 0; } From 16b09eed2a0c638078a01528e2462bdc3718698e Mon Sep 17 00:00:00 2001 From: Alexandre Antonio Juca Date: Fri, 8 May 2026 16:18:07 +0100 Subject: [PATCH 2/2] docs: document keys command --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 98cc26e..1adae88 100644 --- a/README.md +++ b/README.md @@ -116,7 +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 stored keys | +| `KEYS` | `KEYS` | List all non-expired stored keys | ## Documentation