GET /api/browse?path=/some/dir — list subdirectories for file picker */
| 665 | |
| 666 | /* GET /api/browse?path=/some/dir — list subdirectories for file picker */ |
| 667 | static void handle_browse(cbm_http_conn_t *c, const cbm_http_req_t *req) { |
| 668 | char path[1024] = {0}; |
| 669 | const char *home = cbm_get_home_dir(); |
| 670 | if (!cbm_http_query_param(req->query, "path", path, (int)sizeof(path)) || path[0] == '\0') { |
| 671 | /* Default to home directory */ |
| 672 | if (home) |
| 673 | snprintf(path, sizeof(path), "%s", home); |
| 674 | else |
| 675 | snprintf(path, sizeof(path), "/"); |
| 676 | } |
| 677 | |
| 678 | /* The browser UI may send Windows backslash separators (e.g. |
| 679 | * "D:\projects\demo"). Normalize to forward slashes before the cbm_is_dir |
| 680 | * gate, exactly as the MCP repo_path handler and cbm_project_name_from_path |
| 681 | * already do — otherwise a real D:/ directory is rejected (#548). */ |
| 682 | cbm_normalize_path_sep(path); |
| 683 | |
| 684 | if (!cbm_is_dir(path)) { |
| 685 | cbm_http_replyf(c, 400, g_cors_json, "{\"error\":\"not a directory\"}"); |
| 686 | return; |
| 687 | } |
| 688 | |
| 689 | DIR *dir = opendir(path); |
| 690 | if (!dir) { |
| 691 | cbm_http_replyf(c, 403, g_cors_json, "{\"error\":\"cannot open directory\"}"); |
| 692 | return; |
| 693 | } |
| 694 | |
| 695 | /* Build JSON response */ |
| 696 | char buf[32768]; |
| 697 | int pos = 0; |
| 698 | http_appendf(buf, sizeof(buf), &pos, "{\"path\":\"%s\",\"dirs\":[", path); |
| 699 | |
| 700 | struct dirent *ent; |
| 701 | int count = 0; |
| 702 | while ((ent = readdir(dir)) != NULL) { |
| 703 | /* Skip hidden dirs and . / .. */ |
| 704 | if (ent->d_name[0] == '.') |
| 705 | continue; |
| 706 | |
| 707 | /* Check if it's actually a directory */ |
| 708 | char full[2048]; |
| 709 | snprintf(full, sizeof(full), "%s/%s", path, ent->d_name); |
| 710 | if (!cbm_is_dir(full)) |
| 711 | continue; |
| 712 | |
| 713 | if (count > 0) |
| 714 | buf[pos++] = ','; |
| 715 | /* Escape directory name to prevent XSS (e.g., names with quotes/angle brackets) */ |
| 716 | { |
| 717 | char esc[512]; |
| 718 | cbm_json_escape(esc, (int)sizeof(esc), ent->d_name); |
| 719 | http_appendf(buf, sizeof(buf), &pos, "\"%s\"", esc); |
| 720 | } |
| 721 | if (pos >= (int)sizeof(buf)) { |
| 722 | pos = (int)sizeof(buf) - 1; |
| 723 | } |
| 724 | count++; |
no test coverage detected