Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions src/cortex-cli/src/lock_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@
)
}

/// Safely get a string prefix by character count, not byte count.
/// This avoids panics on multi-byte UTF-8 characters.
fn safe_char_prefix(s: &str, max_chars: usize) -> &str {
match s.char_indices().nth(max_chars) {
Some((byte_idx, _)) => &s[..byte_idx],
None => s, // String has fewer than max_chars characters
}
}

/// Get the lock file path.
fn get_lock_file_path() -> PathBuf {
dirs::home_dir()
Expand Down Expand Up @@ -156,7 +165,7 @@
match load_lock_file() {
Ok(lock_file) => lock_file.locked_sessions.iter().any(|entry| {
entry.session_id == session_id
|| session_id.starts_with(&entry.session_id[..8.min(entry.session_id.len())])
|| session_id.starts_with(safe_char_prefix(&entry.session_id, 8))
}),
Err(_) => false,
}
Expand Down Expand Up @@ -300,7 +309,7 @@
serde_json::to_string_pretty(&lock_file.locked_sessions)?
);
} else if lock_file.locked_sessions.is_empty() {
println!("No sessions are locked.");

Check failure

Code scanning / CodeQL

Cleartext logging of sensitive information High

This operation writes
...[...]
to a log file.
println!();
println!("Use 'cortex lock <session-id>' to protect a session.");
} else {
Expand All @@ -308,7 +317,7 @@
println!("{}", "-".repeat(60));

for entry in &lock_file.locked_sessions {
let short_id = &entry.session_id[..8.min(entry.session_id.len())];
let short_id = safe_char_prefix(&entry.session_id, 8);
println!(" {} - locked at {}", short_id, entry.locked_at);
if let Some(ref reason) = entry.reason {
println!(" Reason: {}", reason);
Expand All @@ -332,7 +341,7 @@
e.session_id == args.session_id
|| args
.session_id
.starts_with(&e.session_id[..8.min(e.session_id.len())])
.starts_with(safe_char_prefix(&e.session_id, 8))
});

if is_locked {
Expand All @@ -342,7 +351,7 @@
e.session_id == args.session_id
|| args
.session_id
.starts_with(&e.session_id[..8.min(e.session_id.len())])
.starts_with(safe_char_prefix(&e.session_id, 8))
}) && let Some(ref reason) = entry.reason
{
println!("Reason: {}", reason);
Expand Down Expand Up @@ -508,4 +517,39 @@
let path_str = path.to_string_lossy();
assert!(path_str.contains(".cortex"));
}

#[test]
fn test_safe_char_prefix_ascii() {
// ASCII strings should work correctly
assert_eq!(safe_char_prefix("abcdefghij", 8), "abcdefgh");
assert_eq!(safe_char_prefix("abc", 8), "abc");
assert_eq!(safe_char_prefix("", 8), "");
assert_eq!(safe_char_prefix("12345678", 8), "12345678");
}

#[test]
fn test_safe_char_prefix_utf8_multibyte() {
// Multi-byte UTF-8 characters should not panic
// Each emoji is 4 bytes, so 8 chars = 32 bytes
let emoji_id = "🔥🎉🚀💡🌟✨🎯🔮extra";
assert_eq!(safe_char_prefix(emoji_id, 8), "🔥🎉🚀💡🌟✨🎯🔮");

// Mixed ASCII and multi-byte
let mixed = "ab🔥cd🎉ef";
assert_eq!(safe_char_prefix(mixed, 4), "ab🔥c");
assert_eq!(safe_char_prefix(mixed, 8), "ab🔥cd🎉ef");

// Chinese characters (3 bytes each)
let chinese = "中文测试会话标识符";
assert_eq!(safe_char_prefix(chinese, 4), "中文测试");
}

#[test]
fn test_safe_char_prefix_boundary() {
// Edge cases
assert_eq!(safe_char_prefix("a", 0), "");
assert_eq!(safe_char_prefix("a", 1), "a");
assert_eq!(safe_char_prefix("🔥", 1), "🔥");
assert_eq!(safe_char_prefix("🔥", 0), "");
}
}
Loading