|
63 | 63 |
|
64 | 64 | // --- Data Structures --- |
65 | 65 | typedef struct { unsigned char* data; size_t size; } GzipResult; |
| 66 | +typedef struct { unsigned char* data; size_t size; } Base64DecodeResult; |
66 | 67 | typedef enum { PART_TYPE_TEXT, PART_TYPE_FILE, PART_TYPE_URI } PartType; |
67 | 68 | typedef struct { |
68 | 69 | PartType type; |
@@ -121,6 +122,7 @@ void free_history(History* history); |
121 | 122 | void free_content(Content* content); |
122 | 123 | int get_token_count(AppState* state); |
123 | 124 | char* base64_encode(const unsigned char* data, size_t input_length); |
| 125 | +Base64DecodeResult base64_decode(const char* data); |
124 | 126 | const char* get_mime_type(const char* filename); |
125 | 127 | GzipResult gzip_compress(const unsigned char* input_data, size_t input_size); |
126 | 128 | cJSON* build_request_json(AppState* state); |
@@ -158,6 +160,8 @@ static void process_free_line(char* line, AppState* state); |
158 | 160 | static size_t write_free_memory_callback(void* contents, size_t size, size_t nmemb, void* userp); |
159 | 161 | char* build_free_request_payload(AppState* state, const char* current_prompt, bool is_pro_model); |
160 | 162 |
|
| 163 | +char* process_and_strip_urls(const char* original_prompt, AppState* state); |
| 164 | + |
161 | 165 | /** |
162 | 166 | * @brief Parses a single line from the API's streaming response. |
163 | 167 | * @details This function is designed to handle a Server-Sent Event (SSE) |
@@ -595,6 +599,22 @@ void generate_session(int argc, char* argv[], bool interactive, bool is_stdin_a_ |
595 | 599 | char* p = line; |
596 | 600 | while(isspace((unsigned char)*p)) p++; |
597 | 601 |
|
| 602 | + if (p[0] != '/') { |
| 603 | + // Create a new string with URLs stripped out and attached. |
| 604 | + char* stripped_line = process_and_strip_urls(p, &state); |
| 605 | + |
| 606 | + if (stripped_line) { |
| 607 | + // Free the original line from readline(). |
| 608 | + free(line); |
| 609 | + // Point our main 'line' pointer to the new, cleaned string. |
| 610 | + line = stripped_line; |
| 611 | + // Reset 'p' to point to the start of our new line. |
| 612 | + p = line; |
| 613 | + // Re-trim leading whitespace, in case stripping the URL left some behind. |
| 614 | + while(isspace((unsigned char)*p)) p++; |
| 615 | + } |
| 616 | + } |
| 617 | + |
598 | 618 | // Add non-empty lines to the readline history. |
599 | 619 | if (*p) { |
600 | 620 | #ifndef _WIN32 |
@@ -2300,6 +2320,7 @@ void list_available_models(AppState* state) { |
2300 | 2320 | * the AppState. It formats each user and model turn into a simple Markdown |
2301 | 2321 | * structure, including the system prompt, text content, and placeholders |
2302 | 2322 | * for file attachments. The output is saved to the specified file path. |
| 2323 | + * MODIFIED: It now decodes and prints the content of text/plain attachments. |
2303 | 2324 | * @param state The current application state, containing the history to be exported. |
2304 | 2325 | * @param filepath The path to the Markdown file that will be created or overwritten. |
2305 | 2326 | */ |
@@ -2339,10 +2360,26 @@ void export_history_to_markdown(AppState* state, const char* filepath) { |
2339 | 2360 | fprintf(file, "%s\n", part->text); |
2340 | 2361 | has_text = true; |
2341 | 2362 | } else if (part->type == PART_TYPE_FILE) { |
2342 | | - // For file attachments, write a placeholder indicating the file's name and type. |
2343 | | - const char* filename = part->filename ? part->filename : "Pasted Data"; |
2344 | | - const char* mime_type = part->mime_type ? part->mime_type : "unknown"; |
2345 | | - fprintf(file, "\n`[Attached File: %s (%s)]`\n", filename, mime_type); |
| 2363 | + // --- MODIFICATION START --- |
| 2364 | + // Check if the attachment is text/plain and has data to be decoded. |
| 2365 | + if (part->mime_type && strcmp(part->mime_type, "text/plain") == 0 && part->base64_data) { |
| 2366 | + Base64DecodeResult decoded = base64_decode(part->base64_data); |
| 2367 | + if (decoded.data) { |
| 2368 | + // If decoding is successful, print the content in a formatted block. |
| 2369 | + fprintf(file, "\n```text\n%s\n```\n", (char*)decoded.data); |
| 2370 | + free(decoded.data); // Free the memory from the decoder. |
| 2371 | + } else { |
| 2372 | + // If decoding fails, print a placeholder with an error. |
| 2373 | + const char* filename = part->filename ? part->filename : "Pasted Data"; |
| 2374 | + fprintf(file, "\n`[Attached File: %s (text/plain, decoding failed)]`\n", filename); |
| 2375 | + } |
| 2376 | + } else { |
| 2377 | + // For all other file types, use the original placeholder. |
| 2378 | + const char* filename = part->filename ? part->filename : "Pasted Data"; |
| 2379 | + const char* mime_type = part->mime_type ? part->mime_type : "unknown"; |
| 2380 | + fprintf(file, "\n`[Attached File: %s (%s)]`\n", filename, mime_type); |
| 2381 | + } |
| 2382 | + // --- MODIFICATION END --- |
2346 | 2383 | } |
2347 | 2384 | } |
2348 | 2385 |
|
@@ -4225,6 +4262,71 @@ bool is_youtube_url(const char* url) { |
4225 | 4262 | return (strstr(url, "youtube.com/watch") != NULL || strstr(url, "youtu.be/") != NULL); |
4226 | 4263 | } |
4227 | 4264 |
|
| 4265 | +/** |
| 4266 | + * @brief Scans a prompt for YouTube URLs, attaches them, and returns a new prompt string. |
| 4267 | + * @details This function takes a string, creates a modifiable copy, and iterates |
| 4268 | + * through it to find and process YouTube URLs. For each URL found, it |
| 4269 | + * uses the existing attachment handler and then removes the URL from the |
| 4270 | + * string. This function correctly handles multiple URLs in a single prompt. |
| 4271 | + * @param original_prompt The user's raw input string. |
| 4272 | + * @param state A pointer to the application state to add attachments to. |
| 4273 | + * @return A new, dynamically allocated string containing the prompt with all |
| 4274 | + * YouTube URLs stripped out. The caller is responsible for freeing this memory. |
| 4275 | + * This new string is a distinct allocation from the original_prompt. |
| 4276 | + */ |
| 4277 | +char* process_and_strip_urls(const char* original_prompt, AppState* state) { |
| 4278 | + // Start with a mutable copy of the original prompt. |
| 4279 | + char* processed_prompt = strdup(original_prompt); |
| 4280 | + if (!processed_prompt) return NULL; // Should not happen |
| 4281 | + |
| 4282 | + bool url_was_found_and_removed; |
| 4283 | + do { |
| 4284 | + url_was_found_and_removed = false; |
| 4285 | + char* search_start = processed_prompt; |
| 4286 | + |
| 4287 | + // Find the beginning of a URL. |
| 4288 | + char* url_start = strstr(search_start, "https://"); |
| 4289 | + if (!url_start) { |
| 4290 | + url_start = strstr(search_start, "http://"); |
| 4291 | + } |
| 4292 | + |
| 4293 | + if (url_start) { |
| 4294 | + // Find the end of the URL (a space or the end of the string). |
| 4295 | + char* url_end = url_start; |
| 4296 | + while (*url_end != '\0' && !isspace((unsigned char)*url_end)) { |
| 4297 | + url_end++; |
| 4298 | + } |
| 4299 | + |
| 4300 | + // Temporarily null-terminate the URL to treat it as a separate string. |
| 4301 | + char original_char = *url_end; |
| 4302 | + *url_end = '\0'; |
| 4303 | + |
| 4304 | + // Check if this substring is a valid YouTube URL. |
| 4305 | + if (is_youtube_url(url_start)) { |
| 4306 | + // Attach the URL using the existing handler. |
| 4307 | + handle_attachment_from_stream(NULL, url_start, "video/*", state); |
| 4308 | + |
| 4309 | + // Use the existing str_replace helper to remove the URL. |
| 4310 | + // We pass an empty string "" as the replacement. |
| 4311 | + char* temp_prompt = str_replace(processed_prompt, url_start, ""); |
| 4312 | + |
| 4313 | + // Free the old processed_prompt and point to the new one. |
| 4314 | + free(processed_prompt); |
| 4315 | + processed_prompt = temp_prompt; |
| 4316 | + |
| 4317 | + // Set flag to true so the `do-while` loop runs again, |
| 4318 | + // allowing us to find multiple URLs in the same prompt. |
| 4319 | + url_was_found_and_removed = true; |
| 4320 | + } |
| 4321 | + |
| 4322 | + // Restore the original character if we modified the string. |
| 4323 | + *url_end = original_char; |
| 4324 | + } |
| 4325 | + } while (url_was_found_and_removed); |
| 4326 | + |
| 4327 | + return processed_prompt; |
| 4328 | +} |
| 4329 | + |
4228 | 4330 | /** |
4229 | 4331 | * @brief Reads data from a stream and creates a pending file attachment. |
4230 | 4332 | * @details This function is a robust, production-ready handler for all file and |
@@ -4478,6 +4580,54 @@ char* base64_encode(const unsigned char* data, size_t input_length) { |
4478 | 4580 | return encoded_data; |
4479 | 4581 | } |
4480 | 4582 |
|
| 4583 | +/** |
| 4584 | + * @brief Decodes a Base64 string into binary data. |
| 4585 | + * @param data The null-terminated Base64 string to decode. |
| 4586 | + * @return A Base64DecodeResult struct. The `data` field contains the decoded |
| 4587 | + * bytes and `size` holds the length. The caller is responsible for |
| 4588 | + * freeing the `data` buffer. `data` will be NULL on failure. |
| 4589 | + */ |
| 4590 | +Base64DecodeResult base64_decode(const char* data) { |
| 4591 | + static const int b64_inv_table[] = { |
| 4592 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 4593 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, |
| 4594 | + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, |
| 4595 | + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, |
| 4596 | + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, |
| 4597 | + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, |
| 4598 | + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, |
| 4599 | + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1 |
| 4600 | + }; |
| 4601 | + |
| 4602 | + Base64DecodeResult result = { .data = NULL, .size = 0 }; |
| 4603 | + size_t input_len = strlen(data); |
| 4604 | + |
| 4605 | + if (input_len % 4 != 0) return result; // Invalid Base64 length |
| 4606 | + |
| 4607 | + result.size = input_len / 4 * 3; |
| 4608 | + if (data[input_len - 1] == '=') (result.size)--; |
| 4609 | + if (data[input_len - 2] == '=') (result.size)--; |
| 4610 | + |
| 4611 | + result.data = malloc(result.size + 1); |
| 4612 | + if (!result.data) return result; |
| 4613 | + |
| 4614 | + for (size_t i = 0, j = 0; i < input_len; ) { |
| 4615 | + uint32_t sextet_a = (i < input_len) ? b64_inv_table[(unsigned char)data[i++]] : 0; |
| 4616 | + uint32_t sextet_b = (i < input_len) ? b64_inv_table[(unsigned char)data[i++]] : 0; |
| 4617 | + uint32_t sextet_c = (i < input_len) ? b64_inv_table[(unsigned char)data[i++]] : 0; |
| 4618 | + uint32_t sextet_d = (i < input_len) ? b64_inv_table[(unsigned char)data[i++]] : 0; |
| 4619 | + |
| 4620 | + uint32_t triple = (sextet_a << 18) + (sextet_b << 12) + (sextet_c << 6) + sextet_d; |
| 4621 | + |
| 4622 | + if (j < result.size) result.data[j++] = (triple >> 16) & 0xFF; |
| 4623 | + if (j < result.size) result.data[j++] = (triple >> 8) & 0xFF; |
| 4624 | + if (j < result.size) result.data[j++] = triple & 0xFF; |
| 4625 | + } |
| 4626 | + |
| 4627 | + result.data[result.size] = '\0'; |
| 4628 | + return result; |
| 4629 | +} |
| 4630 | + |
4481 | 4631 | /** |
4482 | 4632 | * @brief Performs the low-level cURL request for the official Gemini API. |
4483 | 4633 | * @details This is the core transport function for all POST requests to the |
|
0 commit comments