Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
*.o
build/
*.test
*.test
*.dSYM/
nutshell
1 change: 1 addition & 0 deletions include/nutshell/theme.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ typedef struct {
// Theme segment structure
typedef struct {
bool enabled;
char *key; // Add this field to store the segment name from JSON
char *format;
int command_count;
ThemeCommand **commands; // Array of commands for this segment
Expand Down
File renamed without changes.
240 changes: 74 additions & 166 deletions src/utils/theme.c
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,10 @@ Theme *load_theme(const char *theme_name) {
ThemeSegment *segment = calloc(1, sizeof(ThemeSegment));
if (!segment) continue;

// Store the segment key from JSON directly
segment->key = strdup(key);
THEME_DEBUG("Loading segment with key: %s", segment->key);

json_t *enabled = json_object_get(value, "enabled");
json_t *format = json_object_get(value, "format");

Expand Down Expand Up @@ -443,6 +447,7 @@ void free_theme(Theme *theme) {
free(theme->prompt_symbol_color);

for (int i = 0; i < theme->segment_count; i++) {
free(theme->segments[i]->key); // Free the segment key
free(theme->segments[i]->format);

// Free all commands in the segment
Expand Down Expand Up @@ -543,194 +548,97 @@ char *get_theme_prompt(Theme *theme) {
left_prompt = str_replace(temp, "{success}", theme->colors->success);
free(temp);

// Process all segments in the prompt
// Extract all segment placeholders from the prompt format
THEME_DEBUG("Extracting segments from prompt format");

// Process all segments in the theme definition
for (int i = 0; i < theme->segment_count; i++) {
ThemeSegment *segment = theme->segments[i];
if (!segment || !segment->enabled) continue;

THEME_DEBUG("Processing segment %d: format = '%s'", i, segment->format);

// Extract the segment key from the segment name/key in the JSON file
// instead of trying to extract it from the format string
char *segment_key = NULL;
if (!segment || !segment->enabled || !segment->key) continue;

// In our JSON format, the segment key is the name used in the segments object
// This is stored in the JSON object's key but we don't have direct access here
// So we extract it from the left_prompt->format and look for a matching segment
// Create the placeholder from the segment key that was stored during load
char placeholder[256];
snprintf(placeholder, sizeof(placeholder), "{%s}", segment->key);
THEME_DEBUG("Checking for placeholder: %s", placeholder);

// First check if this is a well-known segment
if (i == 0 && strstr(segment->format, "{directory}")) {
segment_key = strdup("directory");
} else if (i == 1 && strstr(segment->format, "git:({branch})")) {
segment_key = strdup("git_info"); // Match the key used in the prompt format
} else {
// For other segments, try to infer from format but skip color codes
const char *colors[] = {"primary", "secondary", "success", "error", "warning", "info", "reset"};
const char *format_str = segment->format;
const char *key_start = NULL;
const char *key_end = NULL;
// Only process if the placeholder exists in the prompt
if (strstr(left_prompt, placeholder)) {
Comment on lines +559 to +565
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Risk of buffer overflow for long segment keys.

Using a fixed buffer (char placeholder[256]) and snprintf(placeholder, sizeof(placeholder), "{%s}", segment->key); risks truncation or buffer overflow if a malicious or accidentally large key is encountered. Consider either validating the maximum length of segment->key or dynamically allocating the buffer.

Please run the following script to see if there are any notes or constraints on the maximum key length in the codebase or related documentation:


🏁 Script executed:

#!/bin/bash
# Scours the codebase for references to 'max key length' or 'segment key length'
rg -i "max key length|segment key length|MAX_KEY_LEN|segment->key"

Length of output: 486


Action Required: Mitigate Buffer Overflow Risk in src/utils/theme.c

The investigation did not reveal any constraints on the length of segment->key anywhere in the codebase. As a result, using a fixed-size buffer (char placeholder[256]) for constructing the placeholder poses a real risk of data truncation and potential buffer overflow if a very long key is encountered.

Recommended Actions:

  • Validate Length: Enforce a maximum length on segment->key before using it to format the placeholder.
  • Dynamic Allocation: Alternatively, dynamically allocate the buffer with the exact size needed to avoid hard-coded limits.

Implementing one of these measures will help eliminate the risk associated with processing unusually long segment keys.

THEME_DEBUG("Processing segment: %s", segment->key);

// Find the first bracketed token that is not a color code
const char *pos = format_str;
while ((pos = strchr(pos, '{')) != NULL) {
// Extract potential key
const char *end = strchr(pos, '}');
if (!end) break;

// Check if it's a color code
bool is_color = false;
// Fix sign comparison using size_t for array index
for (size_t c = 0; c < sizeof(colors)/sizeof(colors[0]); c++) {
size_t len = strlen(colors[c]);
// Fix by casting to size_t for comparison
if ((size_t)(end - pos - 1) == len && strncmp(pos + 1, colors[c], len) == 0) {
is_color = true;
break;
}
}

if (!is_color) {
// Found a non-color token
key_start = pos;
key_end = end;
break;
}

pos = end + 1; // Move past this color code
}
// Execute all commands for this segment to get live data
execute_segment_commands(segment);

// Extract the key name
if (key_start && key_end && key_end > key_start) {
int key_len = key_end - key_start - 1;
segment_key = malloc(key_len + 1);
if (segment_key) {
strncpy(segment_key, key_start + 1, key_len);
segment_key[key_len] = '\0';
}
}
// Create formatted segment with all replaced values
char *formatted_segment = strdup(segment->format);
if (!formatted_segment) continue;

// Fallback - use segment index if we couldn't find a key
if (!segment_key) {
segment_key = malloc(20);
if (segment_key) {
sprintf(segment_key, "segment_%d", i);
}
}
}

THEME_DEBUG("Using segment key: '%s'", segment_key ? segment_key : "NULL");

// Only proceed if we have a segment key
if (segment_key) {
// Check if this segment is referenced in the prompt
char placeholder[256];
snprintf(placeholder, sizeof(placeholder), "{%s}", segment_key);
// Replace color codes first
char *temp;
temp = formatted_segment;
formatted_segment = str_replace(temp, "{primary}", theme->colors->primary);
free(temp);

THEME_DEBUG("Looking for placeholder '%s' in prompt: '%s'", placeholder, left_prompt);
temp = formatted_segment;
formatted_segment = str_replace(temp, "{secondary}", theme->colors->secondary);
free(temp);

// Continue with existing processing...
if (strstr(left_prompt, placeholder)) {
THEME_DEBUG("Found placeholder %s in prompt", placeholder);

// Execute all commands for this segment
execute_segment_commands(segment);

// Create formatted segment with all replaced values
char *formatted_segment = strdup(segment->format);
if (!formatted_segment) {
free(segment_key);
continue;
}

// Replace color codes
char *temp;
temp = formatted_segment;
formatted_segment = str_replace(temp, "{primary}", theme->colors->primary);
free(temp);

temp = formatted_segment;
formatted_segment = str_replace(temp, "{secondary}", theme->colors->secondary);
free(temp);
temp = formatted_segment;
formatted_segment = str_replace(temp, "{reset}", theme->colors->reset);
free(temp);

temp = formatted_segment;
formatted_segment = str_replace(temp, "{info}", theme->colors->info);
free(temp);

temp = formatted_segment;
formatted_segment = str_replace(temp, "{warning}", theme->colors->warning);
free(temp);

temp = formatted_segment;
formatted_segment = str_replace(temp, "{error}", theme->colors->error);
free(temp);

temp = formatted_segment;
formatted_segment = str_replace(temp, "{success}", theme->colors->success);
free(temp);

// Replace command outputs in the segment format
bool has_output = false;
for (int j = 0; j < segment->command_count; j++) {
ThemeCommand *cmd = segment->commands[j];
if (!cmd || !cmd->name || !cmd->output) continue;

temp = formatted_segment;
formatted_segment = str_replace(temp, "{reset}", theme->colors->reset);
free(temp);
THEME_DEBUG("Command %s output: %s", cmd->name, cmd->output);

temp = formatted_segment;
formatted_segment = str_replace(temp, "{info}", theme->colors->info);
free(temp);
// Replace {command_name} with its output
char cmd_placeholder[256];
snprintf(cmd_placeholder, sizeof(cmd_placeholder), "{%s}", cmd->name);

temp = formatted_segment;
formatted_segment = str_replace(temp, "{warning}", theme->colors->warning);
formatted_segment = str_replace(temp, cmd_placeholder, cmd->output);
free(temp);

temp = formatted_segment;
formatted_segment = str_replace(temp, "{error}", theme->colors->error);
free(temp);
has_output = true;
}

// Only replace in the prompt if there's actual content
if (has_output) {
THEME_DEBUG("Replacing placeholder %s with: %s", placeholder, formatted_segment);

temp = formatted_segment;
formatted_segment = str_replace(temp, "{success}", theme->colors->success);
temp = left_prompt;
left_prompt = str_replace(temp, placeholder, formatted_segment);
free(temp);

// Replace actual command outputs
bool has_output = false;
THEME_DEBUG("Command count for segment: %d", segment->command_count);

for (int j = 0; j < segment->command_count; j++) {
ThemeCommand *cmd = segment->commands[j];
if (!cmd) {
THEME_DEBUG("Command %d is NULL", j);
continue;
}

THEME_DEBUG("Processing command %d: name='%s', output='%s'",
j, cmd->name ? cmd->name : "NULL",
cmd->output ? cmd->output : "NULL");

if (!cmd->name || !cmd->output) continue;

char cmd_placeholder[256];
snprintf(cmd_placeholder, sizeof(cmd_placeholder), "{%s}", cmd->name);

THEME_DEBUG("Replacing '%s' with: '%s' in format: '%s'",
cmd_placeholder, cmd->output, formatted_segment);

char *temp = formatted_segment;
formatted_segment = str_replace(temp, cmd_placeholder, cmd->output);
free(temp);
has_output = true;

THEME_DEBUG("After replacement: '%s'", formatted_segment);
}

// Only replace the segment if at least one command produced output
if (has_output) {
THEME_DEBUG("Replacing placeholder '%s' in prompt with: '%s'",
placeholder, formatted_segment);

char *temp = left_prompt;
left_prompt = str_replace(temp, placeholder, formatted_segment);
free(temp);

THEME_DEBUG("Prompt after replacement: '%s'", left_prompt);
} else {
// If no command produced output, remove the segment
THEME_DEBUG("No command output, removing placeholder '%s'", placeholder);

char *temp = left_prompt;
left_prompt = str_replace(temp, placeholder, "");
free(temp);
}

free(formatted_segment);
} else {
THEME_DEBUG("Placeholder '%s' NOT found in prompt", placeholder);
// Otherwise remove the placeholder
temp = left_prompt;
left_prompt = str_replace(temp, placeholder, "");
free(temp);
}

free(segment_key);
free(formatted_segment);
}
}

// Append the prompt symbol if it's not already in the format
if (!strstr(theme->left_prompt->format, "{prompt_symbol}")) {
char *prompt_color = NULL;
Expand Down
6 changes: 4 additions & 2 deletions tests/test_theme.c
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ Theme* create_test_theme() {
// Directory segment
theme->segments[0] = calloc(1, sizeof(ThemeSegment));
theme->segments[0]->enabled = true;
theme->segments[0]->key = strdup("directory"); // Add this line to set the segment key
theme->segments[0]->format = strdup("{directory}");

// New format with multiple commands
theme->segments[0]->command_count = 1;
theme->segments[0]->commands = calloc(2, sizeof(ThemeCommand*)); // +1 for NULL terminator
Expand All @@ -61,6 +61,7 @@ Theme* create_test_theme() {
theme->segments[1] = calloc(1, sizeof(ThemeSegment));
theme->segments[1]->enabled = true;
// Use git_info as the segment key to match prompt format
theme->segments[1]->key = strdup("git_info"); // Set key to match prompt format
theme->segments[1]->format = strdup("{secondary}git:({branch}){dirty_flag}{reset}");
Comment on lines 63 to 65
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Inconsistency between test and default theme segment keys

There's a naming inconsistency between the test theme and the default theme JSON. The test uses git_info as the segment key, but the default theme in themes/default.json uses git_branch.


🏁 Script executed:

#!/bin/bash
# Check for inconsistencies between test theme and default theme JSON
echo "Checking segment key naming in default.json:"
grep -A 5 "segments" themes/default.json | grep -A 10 "git"

echo "Checking left prompt format in default.json:"
grep -A 3 "left" themes/default.json | grep "format"

echo "Comparing with test theme usage:"
grep -n "git_info" tests/test_theme.c

Length of output: 991


Action Required: Align Git Key Naming Consistency

The verification confirms that the test theme uses "git_info" (e.g., in the left prompt at line 30 and segment at lines 63–65 in tests/test_theme.c), while the default theme JSON (themes/default.json) still defines the key as "git_branch". Please update one of these to ensure consistency. For example:

  • Option 1: Update themes/default.json to use "git_info" for both the segment key and within the left prompt format.
  • Option 2: Revise the test theme in tests/test_theme.c to reference "git_branch" instead of "git_info".

This change will resolve the naming conflict between the test and the default theme.


theme->segments[1]->command_count = 2;
Expand Down Expand Up @@ -271,7 +272,8 @@ int test_segment_commands() {
printf("DEBUG: Theme segment count: %d\n", theme->segment_count);
printf("DEBUG: Left prompt format: '%s'\n", theme->left_prompt->format);
for (int i = 0; i < theme->segment_count; i++) {
printf("DEBUG: Segment %d format: '%s'\n", i, theme->segments[i]->format);
printf("DEBUG: Segment %d key: '%s', format: '%s'\n", i,
theme->segments[i]->key, theme->segments[i]->format);
}

// Test executing commands for a segment
Expand Down
2 changes: 1 addition & 1 deletion themes/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"prompt": {
"left": {
"format": "{primary}{icon} {directory}{reset}",
"format": "{primary}{icon} {directory} {git_branch}{reset}",
"icon": "🥜"
},
"right": {
Expand Down