Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@AlexStocks
Copy link
Contributor

@AlexStocks AlexStocks commented Oct 23, 2025

Summary by CodeRabbit

  • New Features

    • Added Redis string commands: APPEND, STRLEN, GETRANGE, SETRANGE, SETEX, PSETEX, SETNX, GETSET, and MGET for richer string manipulation, range/substr operations, conditional sets, TTL-based set semantics, and multi-key retrieval.
  • Tests

    • Added comprehensive test suite covering edge cases, type/TTL handling, concurrency, binary/UTF-8 cases, overflow scenarios, and extensive behavior verification for all new string operations.

@github-actions github-actions bot added the ✏️ Feature new feature label Oct 23, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 23, 2025

Walkthrough

Adds nine Redis string commands across cmd and storage: APPEND, STRLEN, GETRANGE, SETRANGE, SETEX, PSETEX, SETNX, GETSET, MGET. Command handlers, storage implementations, multi-instance routing, and comprehensive unit tests were added and wired into the command table.

Changes

Cohort / File(s) Summary
Command implementations
src/cmd/src/append.rs, src/cmd/src/strlen.rs, src/cmd/src/getrange.rs, src/cmd/src/setrange.rs, src/cmd/src/setex.rs, src/cmd/src/psetex.rs, src/cmd/src/setnx.rs, src/cmd/src/getset.rs, src/cmd/src/mget.rs
Nine new Cmd types added. Each defines metadata, a new() constructor, implements Cmd (do_initial / do_cmd) with argv parsing, storage delegation, and RESP mapping (including Redis-style WRONGTYPE handling and integer/bulk responses).
Cmd module and table
src/cmd/src/lib.rs, src/cmd/src/table.rs
New command modules declared in lib.rs; create_command_table extended to register all new command types via register_cmd!.
Storage implementations (per-instance)
src/storage/src/redis_strings.rs
Implemented Redis-like string operations: strlen, getrange, setrange, setex, psetex, setnx, getset, mget, append, set, and related helpers. Added expiration checks, type validation, TTL conversions/validation, bounds checks, per-key locking and updated get() expiry behavior.
Storage routing (cluster/instance-aware)
src/storage/src/storage_impl.rs
Added mget, append, strlen, getrange, setrange, setex, psetex, setnx, getset methods on Storage that compute slot/instance and delegate to per-instance implementations; mget groups keys across instances and aggregates results.
Tests
src/storage/tests/redis_string_test.rs
Large test suite covering append, strlen, getrange, setrange, getset, mget, setnx, setex/psetex, edge cases (empty, UTF‑8, binary), negative indices, bounds/overflow, type errors, TTL/expiration semantics, and concurrency.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant CmdHandler
    participant StorageLayer
    participant Instance
    participant RocksDB

    Client->>CmdHandler: send command (e.g., APPEND key value)
    CmdHandler->>CmdHandler: parse argv, set client key context
    CmdHandler->>StorageLayer: call operation (append/getrange/...)
    alt multi-key (MGET)
        StorageLayer->>StorageLayer: group keys by slot/instance
        StorageLayer->>Instance: delegate per-instance mget(keys_subset)
    else single-key
        StorageLayer->>Instance: delegate operation for key's instance
    end
    Instance->>Instance: check expiration & type, apply mutation/read
    Instance->>RocksDB: read/write persisted value/metadata
    RocksDB-->>Instance: db result
    Instance-->>StorageLayer: Result<T>
    StorageLayer-->>CmdHandler: Result mapped to RespData
    CmdHandler-->>Client: RESP reply (Integer/Bulk/Array/Error)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Rationale: Many new command files follow a consistent pattern (reducing per-file cognitive load), but the storage layer changes are dense (expiry semantics, per-key locking, multi-instance routing) and the test suite is large and varied, increasing review surface.

Possibly related PRs

Suggested reviewers

  • marsevilspirit

Poem

🐰
I hopped through bytes and left my mark,
Appended tails and measured length stark.
Ranges carved and TTLs set tight,
MGET gathered friends by morning light.
A rabbit’s cheer — commands now bright!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: add mget" accurately describes a real component of the changeset—the MGET command implementation is indeed one of the new features added. However, the changeset is substantially broader than the title suggests. The PR adds nine Redis string commands in total: APPEND, STRLEN, GETRANGE, SETRANGE, SETEX, PSETEX, SETNX, GETSET, and MGET, along with their storage layer implementations and comprehensive test coverage. While the title refers to a genuine part of the change, it does not capture the main scope, which is the addition of multiple Redis string operations. A developer scanning PR history would only see "mget" mentioned and might miss the broader feature set being introduced.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch as-mget

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db9213f and fd03700.

📒 Files selected for processing (3)
  • src/cmd/src/mget.rs (1 hunks)
  • src/storage/src/redis_strings.rs (5 hunks)
  • src/storage/src/storage_impl.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/cmd/src/mget.rs
🧰 Additional context used
🧬 Code graph analysis (2)
src/storage/src/storage_impl.rs (3)
src/storage/src/redis_strings.rs (10)
  • mget (495-536)
  • incr_decr (1064-1130)
  • append (369-437)
  • strlen (117-147)
  • getrange (172-230)
  • setrange (258-365)
  • setex (624-663)
  • psetex (690-730)
  • setnx (748-791)
  • getset (814-863)
src/storage/src/storage.rs (2)
  • new (78-81)
  • new (112-127)
src/storage/src/slot_indexer.rs (2)
  • new (40-46)
  • key_to_slot_id (63-65)
src/storage/src/redis_strings.rs (1)
src/storage/src/storage_impl.rs (9)
  • strlen (93-97)
  • getrange (99-103)
  • setrange (105-109)
  • append (87-91)
  • mget (43-79)
  • setex (111-115)
  • psetex (117-121)
  • setnx (123-127)
  • getset (129-133)
🔇 Additional comments (12)
src/storage/src/storage_impl.rs (2)

43-79: LGTM! Multi-instance mget now correctly routes keys and preserves order.

The implementation properly addresses the previous review feedback by grouping keys by their target instance and preserving the original input order. The single-instance fast path optimization is a good addition.


87-133: LGTM! Delegation methods follow consistent routing pattern.

All new delegation methods correctly route operations to the appropriate instance based on the key's slot ID, maintaining consistency with the existing codebase patterns.

src/storage/src/redis_strings.rs (10)

117-147: LGTM! Correct Redis STRLEN semantics.

The implementation correctly handles non-existent keys, expired keys, and type checking, returning 0 in all edge cases as Redis does.


172-230: LGTM! Proper GETRANGE with negative index support.

The implementation correctly normalizes negative indices and handles edge cases (empty strings, start > end) according to Redis semantics.


369-437: LGTM! Correct APPEND with proper metadata handling.

The implementation correctly handles expired keys (treating them as new), preserves metadata for non-expired keys, and includes overflow protection.


452-459: LGTM! Expired keys now correctly return KeyNotFound error.

The updated behavior properly returns an error for expired keys instead of returning stale cached data, aligning with Redis semantics.


495-536: LGTM! MGET correctly returns None for non-existent, expired, or wrong-type keys.

The implementation properly handles Redis MGET semantics by returning None (nil) for any problematic key rather than raising an error. The optimization note about RocksDB multi_get is appropriate.


624-663: LGTM! SETEX with proper TTL validation and overflow protection.

The implementation correctly validates the TTL, converts to microseconds with overflow checking, and efficiently acquires the lock after validation.


690-730: LGTM! PSETEX with proper millisecond TTL handling.

The implementation correctly validates and converts milliseconds to microseconds with overflow protection, maintaining consistency with the SETEX implementation.


748-791: LGTM! Atomic SETNX with correct conditional set logic.

The implementation properly ensures atomicity with locking and correctly returns 0 when key exists (and is not expired) or 1 when successfully set.


814-863: LGTM! Atomic GETSET with proper old value retrieval.

The implementation correctly returns the old value (or None for expired/non-existent keys) while atomically setting the new value under lock protection.


37-37: LGTM! Error imports added for new functionality.

The additional error variants (InvalidFormatSnafu, KeyNotFoundSnafu) are correctly imported for use in the new string operations.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (13)
src/cmd/src/table.rs (1)

71-72: Heads-up: MGET cross-slot behavior.

Current storage routing uses only the first key’s instance (see storage_impl comment). That’s fine for single-instance, but in a sharded/clustered setup MGET should fan out and preserve key order. Consider adding a TODO here linking to a tracking issue for multi-instance fan‑out.

src/cmd/src/mget.rs (1)

70-95: Minor: preallocate response capacity.

Tiny perf tweak: reserve capacity before mapping results.

-            Ok(values) => {
-                // Convert Option<String> to RespData
-                let resp_array: Vec<RespData> = values
+            Ok(values) => {
+                // Convert Option<String> to RespData
+                let mut resp_array: Vec<RespData> = Vec::with_capacity(values.len());
+                resp_array.extend(
+                    values
                     .into_iter()
-                    .map(|opt_val| match opt_val {
-                        Some(val) => RespData::BulkString(Some(val.into_bytes().into())),
-                        None => RespData::BulkString(None),
-                    })
-                    .collect();
+                    .map(|opt_val| match opt_val {
+                        Some(val) => RespData::BulkString(Some(val.into_bytes().into())),
+                        None => RespData::BulkString(None),
+                    })
+                );
 
                 client.set_reply(RespData::Array(Some(resp_array)));
src/cmd/src/append.rs (2)

68-78: Forward all RedisErr messages (consistency with other commands)

Currently only WRONGTYPE is forwarded; others are wrapped. Mirror setex/psetex/getset behavior.

-            Err(e) => match e {
-                storage::error::Error::RedisErr { ref message, .. }
-                    if message.starts_with("WRONGTYPE") =>
-                {
-                    // RedisErr already contains the formatted message
-                    client.set_reply(RespData::Error(message.clone().into()));
-                }
-                _ => {
-                    client.set_reply(RespData::Error(format!("ERR {e}").into()));
-                }
-            },
+            Err(e) => match e {
+                storage::error::Error::RedisErr { ref message, .. } => {
+                    client.set_reply(RespData::Error(message.clone().into()));
+                }
+                _ => client.set_reply(RespData::Error(format!("ERR {e}").into())),
+            },

52-55: Minor: avoid repeated argv cloning

Cache argv once per function to reduce small allocations.

Also applies to: 59-63

src/cmd/src/getset.rs (1)

72-81: Ensure GETSET clears TTL (Redis semantics)

GETSET should overwrite the key and clear any existing expire. Please confirm storage.getset removes TTL; if not, adjust.

Reference: “The timeout will only be cleared by commands that delete or overwrite the contents of the key, including … GETSET …”. (redis.io)

src/cmd/src/setrange.rs (1)

81-86: Optional: cap resulting length before storage call

If storage enforces i32::MAX result length, consider pre-checking offset + value.len() to fail fast and return a user-friendly ERR.

src/storage/tests/redis_string_test.rs (3)

688-739: Add WRONGTYPE coverage for APPEND

This test only exercises string keys. Please add a case where a non-string key (e.g., created via hset) is passed to append and assert WRONGTYPE.


2158-2240: Add GETSET TTL-clearing test

Per Redis, GETSET overwrites and clears any existing expire. Consider a test: SETEX key 1 "v"; then GETSET key "w"; sleep 2s; assert key still exists ("w").

Reference: “The timeout will only be cleared by commands that delete or overwrite the contents of the key, including … GETSET …”. (redis.io)

Also applies to: 2242-2287


741-786: Perf nit: avoid UTF-8 .chars() on large ASCII payloads

Use bytes to validate content and length to reduce overhead.

-assert_eq!(final_value.len(), 10 * 1024 * 1024);
-assert!(final_value.chars().all(|c| c == 'x'));
+assert_eq!(final_value.as_bytes().len(), 10 * 1024 * 1024);
+assert!(final_value.as_bytes().iter().all(|&b| b == b'x'));

Also applies to: 773-779

src/storage/src/storage_impl.rs (1)

63-109: Per-key delegations LGTM; consider a small DRY helper

Straightforward, consistent slot routing. Optionally extract a helper to reduce repetition, e.g., compute instance_id once and invoke a closure.

src/storage/src/redis_strings.rs (3)

486-524: Clippy: replace redundant pattern match with is_err() in MGET

Use is_err() to satisfy lint and simplify logic.

-                    // Check type - if not string type, return None (like Redis does)
-                    if let Err(_) = self.check_type(val.as_slice(), DataType::String) {
+                    // Check type - if not string type, return None (like Redis does)
+                    if self.check_type(val.as_slice(), DataType::String).is_err() {
                         results.push(None);
                         continue;
                     }

Also applies to: 501-505


124-127: Minor cleanup: prefer unwrap_or_default() for Vecs

Replace unwrap_or_else(Vec::new) with unwrap_or_default for brevity.

-    .context(RocksSnafu)?
-    .unwrap_or_else(Vec::new);
+    .context(RocksSnafu)?
+    .unwrap_or_default();

Also applies to: 179-182, 285-288, 380-383, 1064-1067


413-419: Optional: unify error text prefix

Elsewhere you use “ERR …”; here it’s “string exceeds maximum allowed size”. Consider “ERR string exceeds maximum allowed size” for consistency.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 48858eb and db9213f.

📒 Files selected for processing (14)
  • src/cmd/src/append.rs (1 hunks)
  • src/cmd/src/getrange.rs (1 hunks)
  • src/cmd/src/getset.rs (1 hunks)
  • src/cmd/src/lib.rs (1 hunks)
  • src/cmd/src/mget.rs (1 hunks)
  • src/cmd/src/psetex.rs (1 hunks)
  • src/cmd/src/setex.rs (1 hunks)
  • src/cmd/src/setnx.rs (1 hunks)
  • src/cmd/src/setrange.rs (1 hunks)
  • src/cmd/src/strlen.rs (1 hunks)
  • src/cmd/src/table.rs (1 hunks)
  • src/storage/src/redis_strings.rs (5 hunks)
  • src/storage/src/storage_impl.rs (1 hunks)
  • src/storage/tests/redis_string_test.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (14)
src/cmd/src/getrange.rs (2)
src/cmd/src/lib.rs (4)
  • do_initial (109-109)
  • do_initial (237-239)
  • do_cmd (111-111)
  • do_cmd (241-257)
src/client/src/lib.rs (1)
  • argv (77-80)
src/cmd/src/psetex.rs (2)
src/cmd/src/lib.rs (5)
  • new (202-213)
  • do_initial (109-109)
  • do_initial (237-239)
  • do_cmd (111-111)
  • do_cmd (241-257)
src/client/src/lib.rs (1)
  • argv (77-80)
src/cmd/src/getset.rs (2)
src/cmd/src/lib.rs (5)
  • new (202-213)
  • do_initial (109-109)
  • do_initial (237-239)
  • do_cmd (111-111)
  • do_cmd (241-257)
src/client/src/lib.rs (1)
  • argv (77-80)
src/cmd/src/setrange.rs (2)
src/cmd/src/lib.rs (4)
  • do_initial (109-109)
  • do_initial (237-239)
  • do_cmd (111-111)
  • do_cmd (241-257)
src/client/src/lib.rs (1)
  • argv (77-80)
src/cmd/src/lib.rs (2)
src/storage/src/redis_strings.rs (11)
  • append (369-437)
  • get (440-469)
  • getrange (172-230)
  • getset (802-851)
  • mget (486-524)
  • psetex (678-718)
  • set (568-591)
  • setex (612-651)
  • setnx (736-779)
  • setrange (258-365)
  • strlen (117-147)
src/storage/src/storage_impl.rs (11)
  • append (63-67)
  • get (37-41)
  • getrange (75-79)
  • getset (105-109)
  • mget (43-55)
  • psetex (93-97)
  • set (31-35)
  • setex (87-91)
  • setnx (99-103)
  • setrange (81-85)
  • strlen (69-73)
src/cmd/src/setnx.rs (2)
src/cmd/src/lib.rs (5)
  • new (202-213)
  • do_initial (109-109)
  • do_initial (237-239)
  • do_cmd (111-111)
  • do_cmd (241-257)
src/client/src/lib.rs (1)
  • argv (77-80)
src/cmd/src/table.rs (2)
src/storage/src/redis_strings.rs (11)
  • append (369-437)
  • set (568-591)
  • get (440-469)
  • strlen (117-147)
  • getrange (172-230)
  • setrange (258-365)
  • setex (612-651)
  • psetex (678-718)
  • setnx (736-779)
  • getset (802-851)
  • mget (486-524)
src/storage/src/storage_impl.rs (11)
  • append (63-67)
  • set (31-35)
  • get (37-41)
  • strlen (69-73)
  • getrange (75-79)
  • setrange (81-85)
  • setex (87-91)
  • psetex (93-97)
  • setnx (99-103)
  • getset (105-109)
  • mget (43-55)
src/storage/src/storage_impl.rs (3)
src/storage/src/redis_strings.rs (10)
  • mget (486-524)
  • incr_decr (1052-1118)
  • append (369-437)
  • strlen (117-147)
  • getrange (172-230)
  • setrange (258-365)
  • setex (612-651)
  • psetex (678-718)
  • setnx (736-779)
  • getset (802-851)
src/storage/src/storage.rs (2)
  • new (78-81)
  • new (112-127)
src/storage/src/slot_indexer.rs (2)
  • new (40-46)
  • key_to_slot_id (63-65)
src/storage/tests/redis_string_test.rs (4)
src/storage/src/util.rs (1)
  • unique_test_db_path (83-88)
src/storage/src/redis.rs (2)
  • new (96-130)
  • drop (428-436)
src/storage/src/redis_strings.rs (1)
  • get (440-469)
src/storage/src/storage_impl.rs (1)
  • get (37-41)
src/cmd/src/setex.rs (2)
src/cmd/src/lib.rs (5)
  • new (202-213)
  • do_initial (109-109)
  • do_initial (237-239)
  • do_cmd (111-111)
  • do_cmd (241-257)
src/client/src/lib.rs (1)
  • argv (77-80)
src/cmd/src/strlen.rs (2)
src/cmd/src/lib.rs (5)
  • new (202-213)
  • do_initial (109-109)
  • do_initial (237-239)
  • do_cmd (111-111)
  • do_cmd (241-257)
src/client/src/lib.rs (1)
  • argv (77-80)
src/cmd/src/mget.rs (2)
src/cmd/src/lib.rs (5)
  • new (202-213)
  • do_initial (109-109)
  • do_initial (237-239)
  • do_cmd (111-111)
  • do_cmd (241-257)
src/client/src/lib.rs (1)
  • argv (77-80)
src/storage/src/redis_strings.rs (1)
src/storage/src/storage_impl.rs (9)
  • strlen (69-73)
  • getrange (75-79)
  • setrange (81-85)
  • append (63-67)
  • mget (43-55)
  • setex (87-91)
  • psetex (93-97)
  • setnx (99-103)
  • getset (105-109)
src/cmd/src/append.rs (2)
src/cmd/src/lib.rs (4)
  • do_initial (109-109)
  • do_initial (237-239)
  • do_cmd (111-111)
  • do_cmd (241-257)
src/client/src/lib.rs (1)
  • argv (77-80)
🪛 GitHub Actions: ci
src/storage/src/redis_strings.rs

[error] 502-502: redundant pattern matching, consider using is_err()

🪛 GitHub Check: cargo clippy (ubuntu-latest)
src/storage/src/redis_strings.rs

[failure] 502-502:
redundant pattern matching, consider using is_err()

🔇 Additional comments (14)
src/cmd/src/lib.rs (1)

18-33: Module declarations look good.

New modules align with registrations in table.rs and follow existing patterns. One nit: visibility is mixed (older modules like get/set are pub, new ones private). Confirm no external crates rely on re-exporting these command modules.

src/cmd/src/table.rs (1)

55-72: Registrations LGTM.

All new string commands are properly added to the command table and consistent with their module names.

src/cmd/src/mget.rs (2)

32-44: Command meta is correct.

Name/arity/flags/ACL match Redis semantics for MGET.


60-67: Routing via the first key is appropriate here.

This aligns with current storage routing; safe given arity: -2 and prior arg checks.

src/cmd/src/strlen.rs (1)

32-43: Command meta and wiring look correct.

Arity/flags/ACL are accurate; good use of do_initial to set routing key.

src/cmd/src/getrange.rs (2)

32-43: Command meta is accurate.

Name, arity 4, and ACL/flags align with GETRANGE.


58-101: Argument parsing and error handling LGTM.

Clean integer parsing with correct Redis-style error text; result mapping matches storage semantics.

src/cmd/src/psetex.rs (1)

90-94: TTL overflow guard LGTM

The i64::MAX/1000 check prevents ms→µs overflow and aligns with storage-level safety.

src/cmd/src/setnx.rs (1)

75-78: Return mapping LGTM

Integer 1/0 matches Redis semantics for SETNX.

src/cmd/src/setrange.rs (1)

73-77: Pre-validation LGTM

Offset range check with ERR prefix matches tests and avoids needless storage calls.

src/storage/tests/redis_string_test.rs (1)

2502-2543: MGET wrong-type semantics LGTM

Expecting None for wrong-type keys aligns with Redis docs: “For every key that does not hold a string value or does not exist, nil is returned… the operation never fails.”

Reference: Redis MGET. (redis.io)

src/cmd/src/append.rs (1)

51-56: Remove proposed bounds checks—framework validates arity before do_initial

The Cmd trait's execute() method validates argument count via check_arg() (lib.rs:120-142) before calling do_initial(). All commands rely on this framework-level guarantee. The proposed bounds checks in the diff are redundant and inconsistent with the codebase pattern. Only variable-arity commands like mget need internal guards.

Likely an incorrect or invalid review comment.

src/storage/src/redis_strings.rs (2)

453-459: Behavior change: GET now errors on expired keys — ensure command layer maps to nil

Returning KeyNotFoundSnafu for stale keys is fine, but GET should reply with nil (not an error) to clients. Please confirm the cmd layer converts this specific error into a Null Bulk.

If needed, I can add/adjust a test in cmd GET to assert expired → nil.


371-437: LGTM for STRLEN/GETRANGE/APPEND/SETEX/PSETEX/SETNX/GETSET

  • Correct type/expiry checks and per‑key locking where needed.
  • TTL overflow checks look good.

Also applies to: 612-651, 679-718, 736-779, 802-851, 118-147, 173-230

Comment on lines +60 to +65
fn do_initial(&self, client: &Client) -> bool {
let argv = client.argv();
let key = argv[1].clone();
client.set_key(&key);
true
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Guard argv length to avoid panics

Add length checks before indexing argv[1]/[2].

 fn do_initial(&self, client: &Client) -> bool {
-    let argv = client.argv();
+    let argv = client.argv();
+    if argv.len() != 3 {
+        client.set_reply(RespData::Error("ERR wrong number of arguments for command".into()));
+        return false;
+    }
     let key = argv[1].clone();
     client.set_key(&key);
     true
 }
 
 fn do_cmd(&self, client: &Client, storage: Arc<Storage>) {
-    let argv = client.argv();
+    let argv = client.argv();
+    if argv.len() != 3 {
+        client.set_reply(RespData::Error("ERR wrong number of arguments for command".into()));
+        return;
+    }
     let key = client.key();
     let value = &argv[2];

Also applies to: 67-71

🤖 Prompt for AI Agents
In src/cmd/src/getset.rs around lines 60-65 (and also apply the same change to
lines 67-71), the code indexes argv[1] (and argv[2] elsewhere) without checking
argv.len(), which can panic; add explicit length guards (e.g., if argv.len() <=
1 { handle error/return false } and for places needing two args check <=2)
before indexing, return false or send an appropriate client error when the check
fails, and only then safely access argv[1]/argv[2] (retain existing
client.set_key/use logic after the guard).

Comment on lines +62 to +67
fn do_initial(&self, client: &Client) -> bool {
let argv = client.argv();
let key = argv[1].clone();
client.set_key(&key);
true
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add argv length guards to prevent OOB

Indexing argv[1]/[2]/[3] without checking can panic if invoked incorrectly.

 fn do_initial(&self, client: &Client) -> bool {
-    let argv = client.argv();
+    let argv = client.argv();
+    if argv.len() != 4 {
+        client.set_reply(RespData::Error("ERR wrong number of arguments for command".into()));
+        return false;
+    }
     let key = argv[1].clone();
     client.set_key(&key);
     true
 }
 
 fn do_cmd(&self, client: &Client, storage: Arc<Storage>) {
-    let argv = client.argv();
+    let argv = client.argv();
+    if argv.len() != 4 {
+        client.set_reply(RespData::Error("ERR wrong number of arguments for command".into()));
+        return;
+    }
     let key = client.key();
@@
-    let value = &argv[3];
+    let value = &argv[3];

Also applies to: 69-76, 96-103

🤖 Prompt for AI Agents
In src/cmd/src/psetex.rs around lines 62-67 (and similarly at 69-76 and 96-103),
the code indexes argv[1]/[2]/[3] without checking argv.len(), which can panic;
add explicit guards validating argv.len() before accessing those indices
(returning an error or false and logging a helpful message when arguments are
missing), and use safe access (e.g., match or get()) to extract values after the
length check so all argument accesses are bounds-checked and the function fails
gracefully instead of panicking.

Comment on lines +51 to +56
fn do_initial(&self, client: &Client) -> bool {
let argv = client.argv();
let key = argv[1].clone();
client.set_key(&key);
true
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add argv guards and seconds overflow check

Prevent panics and align overflow safety with PSETEX.

 fn do_initial(&self, client: &Client) -> bool {
-    let argv = client.argv();
+    let argv = client.argv();
+    if argv.len() != 4 {
+        client.set_reply(RespData::Error("ERR wrong number of arguments for command".into()));
+        return false;
+    }
     let key = argv[1].clone();
     client.set_key(&key);
     true
 }
 
 fn do_cmd(&self, client: &Client, storage: Arc<Storage>) {
-    let argv = client.argv();
+    let argv = client.argv();
+    if argv.len() != 4 {
+        client.set_reply(RespData::Error("ERR wrong number of arguments for command".into()));
+        return;
+    }
     let key = client.key();
@@
-    // Validate seconds - must be positive
+    // Validate seconds - must be positive
     if seconds <= 0 {
         client.set_reply(RespData::Error("ERR invalid expire time in setex".into()));
         return;
     }
+    // Prevent seconds→microseconds overflow (seconds * 1_000_000)
+    if seconds > i64::MAX / 1_000_000 {
+        client.set_reply(RespData::Error("ERR invalid expire time in setex".into()));
+        return;
+    }

Also applies to: 58-66, 79-86

Comment on lines +60 to +65
fn do_initial(&self, client: &Client) -> bool {
let argv = client.argv();
let key = argv[1].clone();
client.set_key(&key);
true
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add argv guards to avoid panics

Check argv length before indexing.

 fn do_initial(&self, client: &Client) -> bool {
-    let argv = client.argv();
+    let argv = client.argv();
+    if argv.len() != 3 {
+        client.set_reply(RespData::Error("ERR wrong number of arguments for command".into()));
+        return false;
+    }
     let key = argv[1].clone();
     client.set_key(&key);
     true
 }
 
 fn do_cmd(&self, client: &Client, storage: Arc<Storage>) {
-    let argv = client.argv();
+    let argv = client.argv();
+    if argv.len() != 3 {
+        client.set_reply(RespData::Error("ERR wrong number of arguments for command".into()));
+        return;
+    }
     let key = client.key();
     let value = &argv[2];

Also applies to: 67-71

🤖 Prompt for AI Agents
In src/cmd/src/setnx.rs around lines 60 to 65 (and similarly for 67 to 71), the
code indexes argv[1] without checking argv length which can panic; replace
direct indexing with a length check or use argv.get(1) to safely obtain the
argument, return an error/false or handle the missing-argument case (e.g., log
client error and return false) and only call client.set_key(&key) when the key
exists.

Comment on lines +51 to +56
fn do_initial(&self, client: &Client) -> bool {
let argv = client.argv();
let key = argv[1].clone();
client.set_key(&key);
true
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Guard argv length to avoid panics

Add argv length check before indexing.

 fn do_initial(&self, client: &Client) -> bool {
-    let argv = client.argv();
+    let argv = client.argv();
+    if argv.len() != 4 {
+        client.set_reply(RespData::Error("ERR wrong number of arguments for command".into()));
+        return false;
+    }
     let key = argv[1].clone();
     client.set_key(&key);
     true
 }
 
 fn do_cmd(&self, client: &Client, storage: Arc<Storage>) {
-    let key = client.key();
-    let argv = client.argv();
+    let key = client.key();
+    let argv = client.argv();
+    if argv.len() != 4 {
+        client.set_reply(RespData::Error("ERR wrong number of arguments for command".into()));
+        return;
+    }

Also applies to: 58-61

🤖 Prompt for AI Agents
In src/cmd/src/setrange.rs around lines 51-56 (and similarly 58-61), the code
indexes argv without validating its length which can panic; add explicit checks
on client.argv().len() before accessing argv[1] (ensure len() >= 2) and before
later indexes (adjust required minimum for those uses), and if the check fails
return false or an appropriate error path instead of indexing into argv; perform
the length check once per code path (or use pattern matching like
get(1).ok_or(...) semantics) to avoid panics and keep behavior consistent.

Comment on lines +58 to +79
fn do_cmd(&self, client: &Client, storage: Arc<Storage>) {
let key = client.key();

let result = storage.strlen(&key);

match result {
Ok(len) => {
client.set_reply(RespData::Integer(len as i64));
}
Err(e) => match e {
storage::error::Error::RedisErr { ref message, .. }
if message.starts_with("WRONGTYPE") =>
{
// RedisErr already contains the formatted message
client.set_reply(RespData::Error(message.clone().into()));
}
_ => {
client.set_reply(RespData::Error(format!("ERR {e}").into()));
}
},
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Potential overflow from storage layer return type (may yield negative STRLEN).

storage::redis_strings::strlen computes user_value.len() as i32 without bounds check. Since plain SET paths don’t cap value size, values > i32::MAX bytes could produce a negative length here. Either cap string sizes on write (like append/setrange already do) or return i64 from storage for strlen.

Proposed storage-side fix (option A: enforce write-time cap in set to keep len ≤ i32::MAX):

--- a/src/storage/src/redis_strings.rs
+++ b/src/storage/src/redis_strings.rs
@@ pub fn set(&self, key: &[u8], value: &[u8]) -> Result<()> {
-    let string_key = BaseKey::new(key);
+    // Enforce max size consistent with append/setrange
+    if value.len() > i32::MAX as usize {
+        return Err(RedisErr {
+            message: "ERR string exceeds maximum allowed size".to_string(),
+            location: Default::default(),
+        });
+    }
+    let string_key = BaseKey::new(key);

Or (option B: make strlen return i64 safely):

--- a/src/storage/src/redis_strings.rs
+++ b/src/storage/src/redis_strings.rs
- pub fn strlen(&self, key: &[u8]) -> Result<i32> {
+ pub fn strlen(&self, key: &[u8]) -> Result<i64> {
@@
-    Ok(user_value.len() as i32)
+    Ok(user_value.len() as i64)

Note: Option B requires propagating the i64 return up through storage_impl and the command (reply already supports 64-bit integers).

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/cmd/src/strlen.rs around lines 58–79 the code casts the storage strlen
result into i64 after storage currently returns an i32-sized length, which can
overflow/produce negative values for values > i32::MAX; either (A) enforce a
write-time cap so stored values never exceed i32::MAX (modify
set/append/setrange to reject or truncate writes that would exceed i32::MAX and
add tests), or (B — preferred) change the storage API to return i64: update
storage::redis_strings::strlen to compute and return an i64, update the
storage_impl trait/signature and all callers to accept i64, remove any narrowing
casts in this command and pass the returned i64 directly to
client.set_reply(RespData::Integer(len)), and add unit tests to cover very large
values and propagation.

Comment on lines +258 to +365
pub fn setrange(&self, key: &[u8], offset: i64, value: &[u8]) -> Result<i32> {
// Validate offset early to avoid unnecessary database operations
if offset < 0 {
return Err(RedisErr {
message: "ERR offset is out of range".to_string(),
location: Default::default(),
});
}

// Check for offset upper bound to prevent potential overflow
if offset > i32::MAX as i64 {
return Err(RedisErr {
message: "ERR offset is out of range".to_string(),
location: Default::default(),
});
}

let db = self.db.as_ref().context(OptionNoneSnafu {
message: "db is not initialized".to_string(),
})?;

// Get lock for the key
let key_str = String::from_utf8_lossy(key).to_string();
let _lock = ScopeRecordLock::new(self.lock_mgr.as_ref(), &key_str);

let string_key = BaseKey::new(key);
let encode_value = db
.get_opt(&string_key.encode()?, &self.read_options)
.context(RocksSnafu)?
.unwrap_or_else(Vec::new);

let mut existing_value = Vec::new();
let mut ctime: u64 = Utc::now().timestamp_micros() as u64;
let mut etime: u64 = 0;

if !encode_value.is_empty() {
// Check type first to match Redis compatibility
self.check_type(encode_value.as_slice(), DataType::String)?;

let decode_value = ParsedStringsValue::new(&encode_value[..])?;

// If key is not expired, use existing value
if !decode_value.is_stale() {
existing_value = decode_value.user_value().to_vec();
ctime = decode_value.ctime();
etime = decode_value.etime();
}
// If expired, treat as empty string (existing_value remains empty)
}

// Early return optimization: if value is empty and offset is within bounds
if value.is_empty() {
let current_len = existing_value.len() as i32;
// If offset is within current string, no modification needed
if offset <= current_len as i64 {
return Ok(current_len);
}
// If offset is beyond current string, we need to pad
}

let offset_usize = offset as usize;
let current_len = existing_value.len();

// Calculate required length
let required_len = offset_usize + value.len();

// Check for string length overflow
if required_len > i32::MAX as usize {
return Err(RedisErr {
message: "ERR string exceeds maximum allowed size".to_string(),
location: Default::default(),
});
}

let mut new_value = existing_value;

// If offset is beyond current length, pad with zero bytes
if offset_usize > current_len {
new_value.resize(offset_usize, 0);
}

// If new value extends beyond current length, extend the vector
if required_len > new_value.len() {
new_value.resize(required_len, 0);
}

// Overwrite the range with the new value
new_value[offset_usize..offset_usize + value.len()].copy_from_slice(value);

let new_len = new_value.len();

// Set new value with metadata
let mut string_value = StringValue::new(new_value);
string_value.set_ctime(ctime);
string_value.set_etime(etime);

let cf = self
.get_cf_handle(ColumnFamilyIndex::MetaCF)
.context(OptionNoneSnafu {
message: "cf is not initialized".to_string(),
})?;
let mut batch = rocksdb::WriteBatch::default();
batch.put_cf(&cf, string_key.encode()?, string_value.encode());
db.write_opt(batch, &self.write_options)
.context(RocksSnafu)?;

Ok(new_len as i32)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

SETRANGE with empty value incorrectly grows the string with zero-bytes

Redis does not extend the string when value is empty; it performs no write and returns current length even if offset > current length. Current code pads with zeros and changes length.

Fix by unconditionally early‑returning when value.is_empty():

-        // Early return optimization: if value is empty and offset is within bounds
-        if value.is_empty() {
-            let current_len = existing_value.len() as i32;
-            // If offset is within current string, no modification needed
-            if offset <= current_len as i64 {
-                return Ok(current_len);
-            }
-            // If offset is beyond current string, we need to pad
-        }
+        // Redis semantics: empty value must not modify the string (no growth/padding)
+        if value.is_empty() {
+            return Ok(existing_value.len() as i32);
+        }
🤖 Prompt for AI Agents
In src/storage/src/redis_strings.rs around lines 258 to 365, the handler for
SETRANGE currently pads and writes when value.is_empty(), which incorrectly
grows the string; change the logic to unconditionally early-return the current
length (existing_value.len() as i32) when value.is_empty() so no padding,
metadata changes, or RocksDB writes occur (i.e., perform the empty-value check
after existing_value is determined and before any resizing, batch creation, or
db.write calls).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✏️ Feature new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant