feat(config): 插件配置支持 JSON 数组格式

- 极简 JSON 解析器新增 '[' / ']' token 支持
- plugins 字段可同时兼容字符串和字符串数组
- 向后兼容:单字符串 "plugins.so" 仍有效
- 向前扩展:数组 ["a.so", "b.so"] 支持多插件

修复:
- .gitignore 添加遗漏的 test_websocket 二进制
- Makefile 补全 test_log 单元测试编译规则
- cocoon.json 示例改为数组格式
- test_config.c 新增 3 项 plugins 解析测试(字符串/单数组/多数组)

全部 142 个单元测试 + 68 项集成测试通过 ✓
This commit is contained in:
xfy911 2026-06-06 09:07:28 +08:00
parent f2231756a3
commit 2dc29d0687
5 changed files with 91 additions and 8 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ tests/unit/test_log
tests/unit/test_multipart
tests/unit/test_static
tests/unit/test_server
tests/unit/test_websocket
tests/fixtures/uploads/
plugins/*.so
www/uploads/

View File

@ -98,6 +98,9 @@ $(UNIT_TEST_DIR)/test_static: $(UNIT_TEST_DIR)/test_static.c http.c log.c tls.c
$(UNIT_TEST_DIR)/test_websocket: $(UNIT_TEST_DIR)/test_websocket.c websocket.c log.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_websocket.c websocket.c log.c $(UNITY_SRC) $(LDFLAGS)
$(UNIT_TEST_DIR)/test_log: $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_log.c log.c $(UNITY_SRC) -lm
$(UNIT_TEST_DIR)/test_config: $(UNIT_TEST_DIR)/test_config.c config.c log.c access_log.c http.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_config.c config.c log.c access_log.c http.c $(UNITY_SRC) -lm

View File

@ -8,6 +8,6 @@
"log_level": "info",
"gzip_enabled": true,
"brotli_enabled": true,
"plugins": "plugins/hello.so",
"plugins": ["plugins/hello.so"],
"access_log": "-"
}

View File

@ -28,6 +28,8 @@ typedef enum {
TOKEN_NUMBER, /* 123 */
TOKEN_TRUE, /* true */
TOKEN_FALSE, /* false */
TOKEN_LBRACKET, /* [ */
TOKEN_RBRACKET, /* ] */
TOKEN_COMMA, /* , */
TOKEN_COLON, /* : */
TOKEN_INVALID /* 错误 */
@ -89,6 +91,8 @@ static token_t parser_next_token(parser_t *p) {
switch (c) {
case '{': p->pos++; t.type = TOKEN_LBRACE; return t;
case '}': p->pos++; t.type = TOKEN_RBRACE; return t;
case '[': p->pos++; t.type = TOKEN_LBRACKET; return t;
case ']': p->pos++; t.type = TOKEN_RBRACKET; return t;
case ',': p->pos++; t.type = TOKEN_COMMA; return t;
case ':': p->pos++; t.type = TOKEN_COLON; return t;
case '"': {
@ -313,13 +317,36 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) {
} else if (strcmp(key_str, "rate_limit") == 0 && val.type == TOKEN_NUMBER) {
long v = token_to_long(&val);
if (v >= 0 && v < 1000000) config->rate_limit = (uint32_t)v;
} else if (strcmp(key_str, "plugins") == 0 && val.type == TOKEN_STRING) {
} else if (strcmp(key_str, "plugins") == 0) {
if (val.type == TOKEN_STRING) {
char *v = token_str_dup(&val);
if (v && config->num_plugins < COCOON_MAX_PLUGINS) {
config->plugins[config->num_plugins++] = v;
}
} else if (val.type == TOKEN_LBRACKET) {
/* 解析字符串数组 */
while (1) {
token_t item = parser_next_token(&p);
if (item.type == TOKEN_RBRACKET) break;
if (item.type == TOKEN_STRING) {
char *v = token_str_dup(&item);
if (v && config->num_plugins < COCOON_MAX_PLUGINS) {
config->plugins[config->num_plugins++] = v;
}
/* 其他字段:忽略(未来扩展预留) */
} else {
fprintf(stderr, "[Config] 第 %d 行: plugins 数组期望字符串项\n", item.line);
}
token_t sep = parser_next_token(&p);
if (sep.type == TOKEN_RBRACKET) break;
if (sep.type != TOKEN_COMMA) {
fprintf(stderr, "[Config] 第 %d 行: plugins 数组期望 ',' 或 ']'\n", sep.line);
break;
}
}
} else {
fprintf(stderr, "[Config] 第 %d 行: plugins 期望字符串或数组\n", val.line);
}
} /* 其他字段:忽略(未来扩展预留) */
free(key_str);

View File

@ -312,6 +312,55 @@ void test_load_brotli_enabled(void) {
cleanup(p3);
}
void test_load_plugins_string(void) {
const char *p = write_temp_config(
"{\n"
" \"port\": 3000,\n"
" \"plugins\": \"plugins/hello.so\"\n"
"}\n"
);
cocoon_config_t cfg = {0};
TEST_ASSERT_TRUE(config_load_from_file(p, &cfg));
TEST_ASSERT_EQUAL(1, cfg.num_plugins);
TEST_ASSERT_EQUAL_STRING("plugins/hello.so", cfg.plugins[0]);
free((void *)cfg.root_dir);
for (size_t i = 0; i < cfg.num_plugins; i++) free((void *)cfg.plugins[i]);
cleanup(p);
}
void test_load_plugins_array(void) {
const char *p = write_temp_config(
"{\n"
" \"port\": 3000,\n"
" \"plugins\": [\"plugins/a.so\"]\n"
"}\n"
);
cocoon_config_t cfg = {0};
TEST_ASSERT_TRUE(config_load_from_file(p, &cfg));
TEST_ASSERT_EQUAL(1, cfg.num_plugins);
TEST_ASSERT_EQUAL_STRING("plugins/a.so", cfg.plugins[0]);
free((void *)cfg.root_dir);
for (size_t i = 0; i < cfg.num_plugins; i++) free((void *)cfg.plugins[i]);
cleanup(p);
}
void test_load_plugins_array_multiple(void) {
const char *p = write_temp_config(
"{\n"
" \"port\": 3000,\n"
" \"plugins\": [\"plugins/a.so\", \"plugins/b.so\"]\n"
"}\n"
);
cocoon_config_t cfg = {0};
TEST_ASSERT_TRUE(config_load_from_file(p, &cfg));
TEST_ASSERT_EQUAL(2, cfg.num_plugins);
TEST_ASSERT_EQUAL_STRING("plugins/a.so", cfg.plugins[0]);
TEST_ASSERT_EQUAL_STRING("plugins/b.so", cfg.plugins[1]);
free((void *)cfg.root_dir);
for (size_t i = 0; i < cfg.num_plugins; i++) free((void *)cfg.plugins[i]);
cleanup(p);
}
void setUp(void) {}
void tearDown(void) {}
@ -338,5 +387,8 @@ int main(void) {
RUN_TEST(test_load_gzip_enabled);
RUN_TEST(test_load_brotli_enabled);
RUN_TEST(test_load_plugins_string);
RUN_TEST(test_load_plugins_array);
RUN_TEST(test_load_plugins_array_multiple);
return UNITY_END();
}