diff --git a/.gitignore b/.gitignore index b90eeed..b5fdcc2 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/Makefile b/Makefile index 67b67f7..1ece2d7 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/cocoon.json b/cocoon.json index dda7a36..25dfec7 100644 --- a/cocoon.json +++ b/cocoon.json @@ -8,6 +8,6 @@ "log_level": "info", "gzip_enabled": true, "brotli_enabled": true, - "plugins": "plugins/hello.so", - "access_log": "-" + "plugins": ["plugins/hello.so"], + "access_log": "-" } diff --git a/config.c b/config.c index 3e79b57..d6002ec 100644 --- a/config.c +++ b/config.c @@ -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) { - char *v = token_str_dup(&val); - if (v && config->num_plugins < COCOON_MAX_PLUGINS) { - config->plugins[config->num_plugins++] = v; + } 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); diff --git a/tests/unit/test_config.c b/tests/unit/test_config.c index 3b89ea6..6affd2b 100644 --- a/tests/unit/test_config.c +++ b/tests/unit/test_config.c @@ -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(); }