view hg-web/main.c @ 126:e7899c93da77

Remove playground.
author June Park <parkjune1995@gmail.com>
date Thu, 08 Jan 2026 18:03:34 -0800
parents 1c446ab6f945
children ffb764d2fcc5
line wrap: on
line source

#include "seobeo/seobeo.h"
#include "dowa/dowa.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>

#define MAX_PATH 4096

// TODO: Move this to seobeo....
// Asked AI to create this lol, probably should learn to decode it myself..
static void url_decode(char *dst, const char *src)
{
  char a, b;
  while (*src) {
    if ((*src == '%') &&
        ((a = src[1]) && (b = src[2])) &&
        (isxdigit(a) && isxdigit(b))) {
      if (a >= 'a') a -= 'a'-'A';
      if (a >= 'A') a -= ('A' - 10);
      else a -= '0';
      if (b >= 'a') b -= 'a'-'A';
      if (b >= 'A') b -= ('A' - 10);
      else b -= '0';
      *dst++ = 16*a+b;
      src+=3;
    } else if (*src == '+') {
      *dst++ = ' ';
      src++;
    } else {
      *dst++ = *src++;
    }
  }
  *dst = '\0';
}

static int is_directory(const char *path)
{
  struct stat st;
  if (stat(path, &st) != 0) return 0;
  return S_ISDIR(st.st_mode);
}

static int file_exists(const char *path)
{
  struct stat st;
  return stat(path, &st) == 0;
}

static char* sanitize_path(const char *input_path, Dowa_Arena *arena)
{
  if (!input_path || strlen(input_path) == 0)
  {
    char *empty = Dowa_Arena_Allocate(arena, 1);
    empty[0] = '\0';
    return empty;
  }

  size_t len = strlen(input_path);
  char *result = Dowa_Arena_Allocate(arena, len + 1);
  size_t j = 0;

  for (size_t i = 0; i < len; i++)
  {
    if (input_path[i] == '.' && (i == 0 || input_path[i-1] == '/')) {
      if (i + 1 < len && input_path[i+1] == '.') {
        // Skip ".."
        i++;
        continue;
      }
      // Skip "."
      continue;
    }
    result[j++] = input_path[i];
  }
  result[j] = '\0';

  // Remove leading/trailing slashes
  while (result[0] == '/')
    memmove(result, result + 1, strlen(result));
  while (j > 0 && result[j-1] == '/')
    result[--j] = '\0';

  return result;
}

Seobeo_Request_Entry* ApiListDirectory(Seobeo_Request_Entry *req, Dowa_Arena *arena)
{
  Seobeo_Request_Entry *resp = NULL;

  void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path");
  const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : "";

  char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1);
  url_decode(decoded_path, rel_path);

  char *safe_path = sanitize_path(decoded_path, arena);

  Seobeo_Log(SEOBEO_INFO, "rel_path: %s\n", rel_path);
  Seobeo_Log(SEOBEO_INFO, "decoded_path: %s\n", decoded_path);
  Seobeo_Log(SEOBEO_INFO, "safe path: %s\n", safe_path);
  Seobeo_Log(SEOBEO_INFO, "REPO_ROOT: %s\n", REPO_ROOT);
  fflush(stdout);

  char full_path[MAX_PATH];
  if (strlen(safe_path) > 0)
    snprintf(full_path, sizeof(full_path), "%s/%s", REPO_ROOT, safe_path);
  else
    snprintf(full_path, sizeof(full_path), "%s", REPO_ROOT);

  if (!is_directory(full_path))
  {
    char *error_json = Dowa_Arena_Allocate(arena, 256);
    snprintf(error_json, 256, "{\"error\":\"Directory not found\"}");

    Dowa_HashMap_Push_Arena(resp, "status", "404", arena);
    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
    Dowa_HashMap_Push_Arena(resp, "body", error_json, arena);
    return resp;
  }

  DIR *dir = opendir(full_path);
  if (!dir)
  {
    char *error_json = Dowa_Arena_Allocate(arena, 256);
    snprintf(error_json, 256, "{\"error\":\"Cannot open directory\"}");

    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
    Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
    Dowa_HashMap_Push_Arena(resp, "body", error_json, arena);
    return resp;
  }

  char *json = Dowa_Arena_Allocate(arena, 1024 * 100);
  strcpy(json, "{\"files\":[");

  struct dirent *entry;
  int first = 1;

  while ((entry = readdir(dir)) != NULL)
  {
    if (entry->d_name[0] == '.') continue;

    char entry_path[MAX_PATH];
    snprintf(entry_path, sizeof(entry_path), "%s/%s", full_path, entry->d_name);

    int is_dir = is_directory(entry_path);

    char entry_rel_path[MAX_PATH];
    if (strlen(safe_path) > 0)
      snprintf(entry_rel_path, sizeof(entry_rel_path), "%s/%s", safe_path, entry->d_name);
    else
      snprintf(entry_rel_path, sizeof(entry_rel_path), "%s", entry->d_name);

    if (!first) strcat(json, ",");
    first = 0;

    char entry_json[MAX_PATH * 2];
    snprintf(entry_json, sizeof(entry_json),
             "{\"name\":\"%s\",\"type\":\"%s\",\"path\":\"%s\"}",
             entry->d_name,
             is_dir ? "directory" : "file",
             entry_rel_path);
    strcat(json, entry_json);
  }

  closedir(dir);
  strcat(json, "]}");

  Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
  Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
  Dowa_HashMap_Push_Arena(resp, "body", json, arena);

  return resp;
}

Seobeo_Request_Entry* ApiGetFile(Seobeo_Request_Entry *req, Dowa_Arena *arena)
{
  Seobeo_Request_Entry *resp = NULL;

  void *path_kv = Dowa_HashMap_Get_Ptr(req, "query_path");
  const char *rel_path = path_kv ? ((Seobeo_Request_Entry*)path_kv)->value : "";
  char *decoded_path = Dowa_Arena_Allocate(arena, strlen(rel_path) + 1);
  url_decode(decoded_path, rel_path);
  char *safe_path = sanitize_path(decoded_path, arena);

  Seobeo_Log(SEOBEO_INFO, "rel_path: %s\n", rel_path);
  Seobeo_Log(SEOBEO_INFO, "decoded_path: %s\n", decoded_path);
  Seobeo_Log(SEOBEO_INFO, "safe path: %s\n", safe_path);
  Seobeo_Log(SEOBEO_INFO, "REPO_ROOT: %s\n", REPO_ROOT);
  fflush(stdout);

  if (strlen(safe_path) == 0)
  {
    char *error = Dowa_Arena_Allocate(arena, 64);
    strcpy(error, "File path required");

    Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
    Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
    Dowa_HashMap_Push_Arena(resp, "body", error, arena);
    return resp;
  }

  char full_path[MAX_PATH];
  snprintf(full_path, sizeof(full_path), "%s/%s", REPO_ROOT, safe_path);
  FILE *file = fopen(full_path, "rb");
  if (!file)
  {
    char *error_msg = "File not found.";
    Dowa_HashMap_Push_Arena(resp, "status", "404", arena);
    Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
    Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena);
    return resp;
  }

  fseek(file, 0, SEEK_END);
  size_t file_size = ftell(file);
  fseek(file, 0, SEEK_SET);

  char *file_data = malloc(file_size + 1);
  if (!file_data)
  {
    fclose(file);
    char *error_msg = "Memory allocation failed";
    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
    Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
    Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena);
    return resp;
  }

  fread(file_data, 1, file_size, file);
  file_data[file_size] = '\0';
  fclose(file);

  char *body = Dowa_Arena_Allocate(arena, file_size + 1);
  memcpy(body, file_data, file_size);
  body[file_size] = '\0';
  free(file_data);

  if (!body)
  {
    char *error = Dowa_Arena_Allocate(arena, 64);
    strcpy(error, "Cannot read file");

    Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
    Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena);
    Dowa_HashMap_Push_Arena(resp, "body", error, arena);
    return resp;
  }

  const char *content_type = "text/plain";
  if (strstr(safe_path, ".md")) content_type = "text/markdown";
  else if (strstr(safe_path, ".html")) content_type = "text/html";
  else if (strstr(safe_path, ".css")) content_type = "text/css";
  else if (strstr(safe_path, ".js")) content_type = "application/javascript";
  else if (strstr(safe_path, ".json")) content_type = "application/json";

  Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
  Dowa_HashMap_Push_Arena(resp, "content-type", content_type, arena);
  Dowa_HashMap_Push_Arena(resp, "body", body, arena);

  return resp;
}

Seobeo_Request_Entry* ApiGetReadme(Seobeo_Request_Entry *req, Dowa_Arena *arena) {
  return ApiGetFile(req, arena);
}

Seobeo_Request_Entry* ApiHgWireProtocol(Seobeo_Request_Entry *req, Dowa_Arena *arena)
{
    Seobeo_Request_Entry *resp = NULL;

    void *cmd_kv = Dowa_HashMap_Get_Ptr(req, "query_cmd");
    const char *cmd = cmd_kv ? ((Seobeo_Request_Entry*)cmd_kv)->value : "";
    if (strlen(cmd) == 0)
    {
        Dowa_HashMap_Push_Arena(resp, "status", "404", arena);
        return resp;
    }
    Seobeo_Log(SEOBEO_DEBUG, "cmd: %s\n", cmd);

    char command[MAX_PATH];
    snprintf(command, sizeof(command), "hg -R %s serve --stdio 2>&1", REPO_ROOT);
    
    FILE *hg_pipe = popen(command, "r+");
    if (!hg_pipe)
    {
        Seobeo_Log(SEOBEO_DEBUG, "Failed to open pipe\n");
        return resp;
    }
    
    // 2. Write the command
    fprintf(hg_pipe, "capabilities\n");
    fflush(hg_pipe);
    
    // 3. Read the response
    int buffer_size = 1024 * 1024 * 5;
    char *output = Dowa_Arena_Allocate(arena, buffer_size);
    if (fgets(output, buffer_size, hg_pipe) != NULL) {
        Seobeo_Log(SEOBEO_DEBUG, "SUCCESS! Received: %s\n", output);
    } else {
        Seobeo_Log(SEOBEO_DEBUG, "FAILURE: No output received from hg.\n");
    }
    
    // 4. Close and check exit code
    int status = pclose(hg_pipe);
    Seobeo_Log(SEOBEO_DEBUG, "Process exited with status: %d\n", status);

    Seobeo_Log(SEOBEO_DEBUG, "body: %s\n", output);

    Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
    Dowa_HashMap_Push_Arena(resp, "content-type", "application/mercurial-0.2", arena);    
    Dowa_HashMap_Push_Arena(resp, "body", output, arena);

    return resp;
}

int main(void) {
  Seobeo_Router_Init();

  Seobeo_Router_Register("GET", "/api/repo/list", ApiListDirectory);
  Seobeo_Router_Register("GET", "/api/repo/file", ApiGetFile);
  Seobeo_Router_Register("GET", "/api/repo/readme", ApiGetReadme);

  Seobeo_Router_Register("GET", "/repo", ApiHgWireProtocol);
  Seobeo_Router_Register("POST", "/repo", ApiHgWireProtocol);

  printf("Starting on Port 6970...\n");
  printf("Repository: %s\n", REPO_ROOT);

  int result = Seobeo_Web_Server_Start("hg-web/src", "6970", SEOBEO_MODE_EDGE, 4);

  Seobeo_Router_Destroy();

  return result;
}