diff seobeo/s_http_client.c @ 279:b3b547563ec7

Add Google connector service and agent wiki Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code. Co-authored-by: Copilot <[email protected]> Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:22:36 -0700
parents ce7f4400c2de
children
line wrap: on
line diff
--- a/seobeo/s_http_client.c	Mon Aug 17 22:16:14 2026 -0700
+++ b/seobeo/s_http_client.c	Mon Aug 17 22:22:36 2026 -0700
@@ -42,6 +42,90 @@
   return NULL;
 }
 
+static boolean Seobeo_Client_Header_Has_Token(
+    const char *value, const char *token)
+{
+  if (!value || !token)
+    return FALSE;
+  size_t token_length = strlen(token);
+  const char *cursor = value;
+  while (*cursor)
+  {
+    while (*cursor == ' ' || *cursor == '\t' || *cursor == ',')
+      cursor++;
+    const char *end = cursor;
+    while (*end && *end != ',')
+      end++;
+    const char *trimmed_end = end;
+    while (trimmed_end > cursor &&
+           (trimmed_end[-1] == ' ' || trimmed_end[-1] == '\t'))
+      trimmed_end--;
+    if ((size_t)(trimmed_end - cursor) == token_length &&
+        strncasecmp(cursor, token, token_length) == 0)
+      return TRUE;
+    cursor = end;
+  }
+  return FALSE;
+}
+
+static boolean Seobeo_Client_Decode_Chunked(
+    const char *encoded, size_t encoded_length,
+    char *decoded, size_t decoded_capacity, size_t *decoded_length)
+{
+  if (!encoded || !decoded || !decoded_length)
+    return FALSE;
+  size_t input_offset = 0;
+  size_t output_offset = 0;
+  while (input_offset < encoded_length)
+  {
+    size_t line_end = input_offset;
+    while (line_end + 1 < encoded_length &&
+           !(encoded[line_end] == '\r' && encoded[line_end + 1] == '\n'))
+      line_end++;
+    if (line_end + 1 >= encoded_length)
+      return FALSE;
+
+    size_t chunk_size = 0;
+    boolean have_digit = FALSE;
+    for (size_t i = input_offset; i < line_end && encoded[i] != ';'; i++)
+    {
+      uint8 c = (uint8)encoded[i];
+      int32 digit;
+      if (c >= '0' && c <= '9')
+        digit = c - '0';
+      else if (c >= 'a' && c <= 'f')
+        digit = c - 'a' + 10;
+      else if (c >= 'A' && c <= 'F')
+        digit = c - 'A' + 10;
+      else
+        return FALSE;
+      if (chunk_size > (SIZE_MAX - (size_t)digit) / 16)
+        return FALSE;
+      chunk_size = chunk_size * 16 + (size_t)digit;
+      have_digit = TRUE;
+    }
+    if (!have_digit)
+      return FALSE;
+    input_offset = line_end + 2;
+    if (chunk_size == 0)
+    {
+      *decoded_length = output_offset;
+      return TRUE;
+    }
+    if (chunk_size > encoded_length - input_offset ||
+        chunk_size > decoded_capacity - output_offset)
+      return FALSE;
+    memcpy(decoded + output_offset, encoded + input_offset, chunk_size);
+    output_offset += chunk_size;
+    input_offset += chunk_size;
+    if (input_offset + 1 >= encoded_length ||
+        encoded[input_offset] != '\r' || encoded[input_offset + 1] != '\n')
+      return FALSE;
+    input_offset += 2;
+  }
+  return FALSE;
+}
+
 static void Seobeo_Client_Parse_Url(const char *url, char **p_host, 
     char **p_port, char **p_path, boolean *p_use_tls, Dowa_Arena *p_arena)
 {
@@ -405,7 +489,11 @@
   size_t body_len = 0;
   const char *content_length = Seobeo_Client_Header_Value(
       p_resp->headers, "Content-Length");
-  if (content_length)
+  const char *transfer_encoding = Seobeo_Client_Header_Value(
+      p_resp->headers, "Transfer-Encoding");
+  boolean is_chunked = Seobeo_Client_Header_Has_Token(
+      transfer_encoding, "chunked");
+  if (content_length && !is_chunked)
   {
     body_len = (size_t)strtoull(content_length, NULL, 10);
   }
@@ -484,7 +572,8 @@
   {
     size_t cap = 1024 * 1024 * 5;
     size_t used = 0;
-    char *body = download_path ? NULL : Dowa_Arena_Allocate(p_resp->p_arena, cap);
+    char *body = (!download_path || is_chunked)
+        ? Dowa_Arena_Allocate(p_resp->p_arena, cap) : NULL;
 
     while (1)
     {
@@ -494,7 +583,7 @@
       if (n > 0)
       {
         last_progress_ms = Seobeo_Client_Monotonic_Milliseconds();
-        if (download_path)
+        if (download_path && !is_chunked)
         {
           fwrite(p_handle->read_buffer, 1, p_handle->read_buffer_len, p_file);
           used += p_handle->read_buffer_len;
@@ -533,7 +622,31 @@
       }
     }
 
-    if (!download_path)
+    if (is_chunked)
+    {
+      char *decoded = Dowa_Arena_Allocate(p_resp->p_arena, used + 1);
+      size_t decoded_length = 0;
+      if (!decoded ||
+          !Seobeo_Client_Decode_Chunked(
+              body, used, decoded, used, &decoded_length))
+      {
+        if (p_file) fclose(p_file);
+        Seobeo_Client_Response_Destroy(p_resp);
+        return NULL;
+      }
+      decoded[decoded_length] = '\0';
+      if (download_path)
+      {
+        fwrite(decoded, 1, decoded_length, p_file);
+        p_resp->body_length = decoded_length;
+      }
+      else
+      {
+        p_resp->body = decoded;
+        p_resp->body_length = decoded_length;
+      }
+    }
+    else if (!download_path)
     {
       p_resp->body = body;
       p_resp->body_length = used;