From 8ee487f64d11a43e930ceb35b0acf6c677fa9b2f Mon Sep 17 00:00:00 2001 From: xfy911 Date: Thu, 4 Jun 2026 00:18:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(multipart):=20=E6=96=B0=E5=A2=9E=20multipa?= =?UTF-8?q?rt/form-data=20=E8=A7=A3=E6=9E=90=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - multipart.h: 定义 multipart_part_t 结构体 + parse/extract_boundary/free API - multipart.c: 轻量级解析器实现 - find_line_end: 查找行尾(\n 或文件尾) - parse_content_disposition: 解析 name/filename 字段 - multipart_parse: 两阶段解析(先计数再提取),跳过 preamble - multipart_extract_boundary: 从 Content-Type 提取 boundary 值 - multipart_parts_free: 释放所有分配的字段内存 - 零外部依赖,仅使用标准库 --- multipart.c | 293 ++++++++++++++++++++++++++++++++++++++++++++++++++++ multipart.h | 58 +++++++++++ 2 files changed, 351 insertions(+) create mode 100644 multipart.c create mode 100644 multipart.h diff --git a/multipart.c b/multipart.c new file mode 100644 index 0000000..0bf25e0 --- /dev/null +++ b/multipart.c @@ -0,0 +1,293 @@ +/** + * multipart.c - multipart/form-data 解析实现 + * + * 轻量级解析器,支持文件上传。 + * + * @author xfy + */ + +#include "multipart.h" +#include +#include +#include + +/** + * multipart_extract_boundary - 从 Content-Type 提取 boundary + */ +bool multipart_extract_boundary(const char *content_type, char *buf, size_t buf_size) { + if (!content_type || !buf || buf_size == 0) return false; + + const char *p = strstr(content_type, "boundary="); + if (!p) return false; + + p += 9; /* skip "boundary=" */ + + /* 跳过可能的引号 */ + size_t len = 0; + if (*p == '"') { + p++; + while (p[len] && p[len] != '"') len++; + } else { + while (p[len] && p[len] != ';' && p[len] != ' ' && p[len] != '\r' && p[len] != '\n') len++; + } + + if (len == 0 || len >= buf_size) return false; + + memcpy(buf, p, len); + buf[len] = '\0'; + return true; +} + +/** + * find_line_end - 查找行尾(\r\n 或 \n) + * + * @param data 数据指针 + * @param len 剩余长度 + * @return 行尾后的位置偏移,未找到返回 len + */ +static size_t find_line_end(const char *data, size_t len) { + for (size_t i = 0; i < len; i++) { + if (data[i] == '\n') return i + 1; + } + return len; +} + +/** + * parse_content_disposition - 解析 Content-Disposition 头 + * + * @param line 头行内容 + * @param name 输出 name 缓冲区 + * @param name_size name 缓冲区大小 + * @param filename 输出 filename 缓冲区 + * @param filename_size filename 缓冲区大小 + */ +static void parse_content_disposition(const char *line, + char *name, size_t name_size, + char *filename, size_t filename_size) { + const char *p = strstr(line, "name="); + if (p) { + p += 5; + if (*p == '"') { + p++; + size_t i = 0; + while (*p && *p != '"' && i < name_size - 1) { + name[i++] = *p++; + } + name[i] = '\0'; + } else { + size_t i = 0; + while (*p && *p != ';' && *p != ' ' && *p != '\r' && *p != '\n' && i < name_size - 1) { + name[i++] = *p++; + } + name[i] = '\0'; + } + } + + p = strstr(line, "filename="); + if (p) { + p += 9; + if (*p == '"') { + p++; + size_t i = 0; + while (*p && *p != '"' && i < filename_size - 1) { + filename[i++] = *p++; + } + filename[i] = '\0'; + } else { + size_t i = 0; + while (*p && *p != ';' && *p != ' ' && *p != '\r' && *p != '\n' && i < filename_size - 1) { + filename[i++] = *p++; + } + filename[i] = '\0'; + } + } +} + +/** + * multipart_parse - 解析 multipart/form-data 请求体 + */ +int multipart_parse(const char *body, size_t body_len, const char *boundary, + multipart_part_t **parts, int *num_parts) { + if (!body || body_len == 0 || !boundary || !parts || !num_parts) return -1; + + *parts = NULL; + *num_parts = 0; + + /* 构建边界分隔符:--boundary\r\n */ + char delim[256]; + int delim_len = snprintf(delim, sizeof(delim), "--%s", boundary); + if (delim_len < 0 || (size_t)delim_len >= sizeof(delim)) return -1; + + size_t b_len = (size_t)delim_len; + + /* 第一遍:统计部分数量 */ + int count = 0; + const char *p = body; + size_t remain = body_len; + + /* 跳过 preamble(边界前的内容) */ + while (remain >= b_len) { + if (strncmp(p, delim, b_len) == 0) break; + p++; + remain--; + } + + while (remain >= b_len) { + if (strncmp(p, delim, b_len) == 0) { + count++; + p += b_len; + remain -= b_len; + /* 跳过 -- 结束标记 */ + if (remain >= 2 && strncmp(p, "--", 2) == 0) break; + /* 跳过 \r\n 或 \n */ + if (remain >= 2 && p[0] == '\r' && p[1] == '\n') { + p += 2; + remain -= 2; + } else if (remain >= 1 && p[0] == '\n') { + p += 1; + remain -= 1; + } + } else { + p++; + remain--; + } + } + + if (count == 0) return -1; + + /* 分配数组 */ + *parts = (multipart_part_t *)calloc((size_t)count, sizeof(multipart_part_t)); + if (!*parts) return -1; + + /* 第二遍:提取每个部分 */ + int idx = 0; + p = body; + remain = body_len; + + while (remain >= b_len && idx < count) { + /* 查找下一个边界 */ + if (strncmp(p, delim, b_len) != 0) { + p++; + remain--; + continue; + } + + p += b_len; + remain -= b_len; + + /* 检查结束标记 */ + if (remain >= 2 && strncmp(p, "--", 2) == 0) break; + + /* 跳过分隔线后的换行 */ + if (remain >= 2 && p[0] == '\r' && p[1] == '\n') { + p += 2; + remain -= 2; + } else if (remain >= 1 && p[0] == '\n') { + p += 1; + remain -= 1; + } + + /* 解析头部 */ + char name[256] = {0}; + char filename[256] = {0}; + char content_type[128] = {0}; + + while (remain > 0) { + size_t line_end = find_line_end(p, remain); + size_t line_len = line_end; + + /* 空行表示头部结束 */ + if (line_len == 2 && p[0] == '\r' && p[1] == '\n') { + p += 2; + remain -= 2; + break; + } + if (line_len == 1 && p[0] == '\n') { + p += 1; + remain -= 1; + break; + } + + /* 解析头部 */ + if (strncasecmp(p, "Content-Disposition:", 20) == 0) { + parse_content_disposition(p, name, sizeof(name), filename, sizeof(filename)); + } else if (strncasecmp(p, "Content-Type:", 13) == 0) { + const char *vp = p + 13; + while (*vp == ' ' || *vp == '\t') vp++; + size_t ct_len = line_len - (size_t)(vp - p); + /* 去掉尾部 \r\n */ + if (ct_len >= 2 && vp[ct_len - 2] == '\r' && vp[ct_len - 1] == '\n') ct_len -= 2; + else if (ct_len >= 1 && vp[ct_len - 1] == '\n') ct_len -= 1; + if (ct_len > 0 && ct_len < sizeof(content_type)) { + memcpy(content_type, vp, ct_len); + content_type[ct_len] = '\0'; + } + } + + p += line_end; + remain -= line_end; + } + + /* 查找数据结束位置(下一个边界) */ + const char *data_start = p; + size_t data_remain = remain; + size_t data_len = 0; + + while (data_remain >= b_len) { + if (strncmp(p, delim, b_len) == 0) break; + p++; + data_remain--; + data_len++; + } + + /* 更新 remain 以反映已消费的数据 */ + remain = data_remain; + + /* 去掉数据尾部的 \r\n(如果存在) */ + if (data_len >= 2 && data_start[data_len - 2] == '\r' && data_start[data_len - 1] == '\n') { + data_len -= 2; + } else if (data_len >= 1 && data_start[data_len - 1] == '\n') { + data_len -= 1; + } + + /* 保存部分 */ + multipart_part_t *part = &(*parts)[idx]; + if (name[0]) { + part->name = strdup(name); + } + if (filename[0]) { + part->filename = strdup(filename); + } + if (content_type[0]) { + part->content_type = strdup(content_type); + } + if (data_len > 0) { + part->data = (char *)malloc(data_len + 1); + if (part->data) { + memcpy(part->data, data_start, data_len); + part->data[data_len] = '\0'; + part->data_len = data_len; + } + } + + idx++; + } + + *num_parts = idx; + return 0; +} + +/** + * multipart_parts_free - 释放 multipart 解析结果 + */ +void multipart_parts_free(multipart_part_t *parts, int num_parts) { + if (!parts) return; + for (int i = 0; i < num_parts; i++) { + free(parts[i].name); + free(parts[i].filename); + free(parts[i].content_type); + free(parts[i].data); + } + free(parts); +} + diff --git a/multipart.h b/multipart.h new file mode 100644 index 0000000..7cb5497 --- /dev/null +++ b/multipart.h @@ -0,0 +1,58 @@ +/** + * multipart.h - multipart/form-data 解析模块 + * + * 轻量级 multipart 解析器,支持文件上传场景。 + * 零外部依赖,只处理 Cocoon 需要的字段。 + * + * @author xfy + */ + +#ifndef COCOON_MULTIPART_H +#define COCOON_MULTIPART_H + +#include +#include + +/** + * multipart_part_t - 单个 multipart 部分 + */ +typedef struct { + char *name; /**< 字段名 */ + char *filename; /**< 文件名(文件上传时) */ + char *content_type; /**< 部分的 Content-Type */ + char *data; /**< 部分数据 */ + size_t data_len; /**< 数据长度 */ +} multipart_part_t; + +/** + * multipart_parse - 解析 multipart/form-data 请求体 + * + * @param body 请求体数据 + * @param body_len 请求体长度 + * @param boundary 边界字符串(不含前导 --) + * @param parts 输出部分数组(需调用者 free) + * @param num_parts 输出部分数量 + * @return 0 成功,-1 错误 + */ +int multipart_parse(const char *body, size_t body_len, const char *boundary, + multipart_part_t **parts, int *num_parts); + +/** + * multipart_parts_free - 释放 multipart 解析结果 + * + * @param parts 部分数组 + * @param num_parts 部分数量 + */ +void multipart_parts_free(multipart_part_t *parts, int num_parts); + +/** + * multipart_extract_boundary - 从 Content-Type 提取 boundary + * + * @param content_type Content-Type 头值 + * @param buf 输出缓冲区 + * @param buf_size 缓冲区大小 + * @return true 成功提取 + */ +bool multipart_extract_boundary(const char *content_type, char *buf, size_t buf_size); + +#endif /* COCOON_MULTIPART_H */