diff --git a/src/uvhttp_lru_cache.c b/src/uvhttp_lru_cache.c index bd1d9b5c..34f0d027 100644 --- a/src/uvhttp_lru_cache.c +++ b/src/uvhttp_lru_cache.c @@ -689,21 +689,43 @@ void uvhttp_lru_cache_set_max_file_size(cache_manager_t* cache, /** * set maximum memory usage for cache */ +/* Range is enforced at the write point, not only at create time. + * uvhttp_lru_cache_create rejects max_memory_usage == 0 ("cannot be zero"), + * and the eviction path reads the limit as "enabled only when > 0". A setter + * that accepted 0 therefore silently disabled the memory ceiling on a live + * cache — a value create refuses to build in the first place. */ void uvhttp_lru_cache_set_max_memory_usage(cache_manager_t* cache, size_t max_memory_usage) { if (!cache) return; + if (max_memory_usage == 0) { + UVHTTP_LOG_WARN( + "Ignoring max_memory_usage=0 (create rejects it; 0 would disable " + "the memory ceiling)"); + return; + } + cache->max_memory_usage = max_memory_usage; } /** * set maximum entries for cache */ +/* Same rule as create: max_entries must be positive. The eviction path + * treats "> 0" as "limit enabled", so accepting 0 here would turn a bounded + * cache into an unbounded one behind the caller's back. */ void uvhttp_lru_cache_set_max_entries(cache_manager_t* cache, int max_entries) { if (!cache) return; + if (max_entries <= 0) { + UVHTTP_LOG_WARN( + "Ignoring max_entries=%d (must be positive, same rule as create)", + max_entries); + return; + } + cache->max_entries = max_entries; } diff --git a/src/uvhttp_response.c b/src/uvhttp_response.c index 2957e404..feb121ca 100644 --- a/src/uvhttp_response.c +++ b/src/uvhttp_response.c @@ -463,18 +463,23 @@ uvhttp_error_t uvhttp_response_set_body(uvhttp_response_t* response, return UVHTTP_ERROR_INVALID_PARAM; } - if (response->body) { - uvhttp_free(response->body); - response->body = NULL; - } - - response->body = uvhttp_alloc(length); - if (!response->body) { - response->body_length = 0; + /* Atomic replace: allocate the new body FIRST, then drop the old one. + * + * The previous order (free old -> alloc new) lost the old body whenever + * the allocation failed: the caller got UVHTTP_ERROR_OUT_OF_MEMORY and + * a response with body=NULL, so a retry had nothing to fall back on. + * set_header already had this property for free because uvhttp_realloc + * keeps the old block when it fails; set_body allocates a fresh block, + * so it has to buy the atomicity explicitly. */ + char* new_body = uvhttp_alloc(length); + if (!new_body) { return UVHTTP_ERROR_OUT_OF_MEMORY; } + memcpy(new_body, body, length); - memcpy(response->body, body, length); + /* Only now is the old body expendable. */ + uvhttp_free(response->body); + response->body = new_body; response->body_length = length; return UVHTTP_OK; diff --git a/test/unit/test_lru_cache_capacity.cpp b/test/unit/test_lru_cache_capacity.cpp index 67db090b..e3c70a0a 100644 --- a/test/unit/test_lru_cache_capacity.cpp +++ b/test/unit/test_lru_cache_capacity.cpp @@ -122,4 +122,78 @@ TEST_F(LruCacheCapacity, EntryJustUnderThresholdIsAccepted) { EXPECT_EQ(cache_->entry_count, 1); } -#endif /* UVHTTP_FEATURE_STATIC_FILES */ \ No newline at end of file +/* + * 背景:uvhttp_lru_cache_create 拒绝 max_memory_usage == 0("cannot be zero") + * 且要求 max_entries > 0,而淘汰路径把 "> 0" 读作"限额生效"。setter 此前 + * 直接赋值,于是两条写路径对同一个不变量给出相反答案:create 拒绝的值, + * setter 照单全收,还顺手把运行中缓存的限额关掉了(0 → 视为无限额)。 + * + * 同一个库内的正确范式是 uvhttp_lru_cache_set_batch_eviction_size:越界时 + * 告警并保持原值,而不是静默接受。 + */ + +TEST_F(LruCacheCapacity, SetMaxMemoryUsageRejectsZero) { + Create(kBudget); + + uvhttp_lru_cache_set_max_memory_usage(cache_, 0); + + EXPECT_EQ(cache_->max_memory_usage, kBudget) + << "create 拒绝 0(cannot be zero),setter 不得把它写进去"; +} + +/* 越界值不生效后,限额仍是原来那个 —— 不是被静默放宽 */ +TEST_F(LruCacheCapacity, ZeroMaxMemoryUsageDoesNotWidenTheBudget) { + Create(kBudget); + + uvhttp_lru_cache_set_max_memory_usage(cache_, 0); + + /* 预算仍是 16KB:8KB 内容条目(+约 10KB 开销)依旧放不进 */ + EXPECT_EQ(Put("/big.bin", 8 * 1024, 'x'), UVHTTP_ERROR_OUT_OF_MEMORY) + << "写入 0 之后内存上限必须仍然生效"; +} + +TEST_F(LruCacheCapacity, SetMaxMemoryUsageAcceptsPositiveValue) { + Create(kBudget); + + uvhttp_lru_cache_set_max_memory_usage(cache_, 32 * 1024); + + EXPECT_EQ(cache_->max_memory_usage, 32 * 1024); + EXPECT_EQ(Put("/big.bin", 8 * 1024, 'x'), UVHTTP_OK) + << "合法的更大预算应立即生效"; +} + +TEST_F(LruCacheCapacity, SetMaxEntriesRejectsZeroAndNegative) { + Create(kBudget); + + uvhttp_lru_cache_set_max_entries(cache_, 0); + EXPECT_EQ(cache_->max_entries, 10) << "create 要求 max_entries > 0"; + + uvhttp_lru_cache_set_max_entries(cache_, -5); + EXPECT_EQ(cache_->max_entries, 10) << "负值同样必须被拒"; +} + +/* 关闭条目限额会让有界缓存退化成无界缓存 —— 这正是校验要阻止的后果 */ +TEST_F(LruCacheCapacity, ZeroMaxEntriesWouldDisableTheEntryCeiling) { + Create(kBudget); + + uvhttp_lru_cache_set_max_entries(cache_, 0); + + /* 限额仍是 10:放入第 11 条时应触发淘汰,entry_count 不越过上限 */ + for (int i = 0; i < 11; i++) { + char path[32]; + snprintf(path, sizeof(path), "/f%d.bin", i); + Put(path, 64, 's'); + } + EXPECT_LE(cache_->entry_count, cache_->max_entries) + << "条目上限必须仍然生效"; +} + +TEST_F(LruCacheCapacity, SetMaxEntriesAcceptsPositiveValue) { + Create(kBudget); + + uvhttp_lru_cache_set_max_entries(cache_, 3); + + EXPECT_EQ(cache_->max_entries, 3); +} + +#endif /* UVHTTP_FEATURE_STATIC_FILES */ diff --git a/test/unit/test_response_full_coverage_enhanced.cpp b/test/unit/test_response_full_coverage_enhanced.cpp index 07278752..5184715c 100644 --- a/test/unit/test_response_full_coverage_enhanced.cpp +++ b/test/unit/test_response_full_coverage_enhanced.cpp @@ -1,9 +1,9 @@ /** * @file test_response_full_coverage_enhanced.cpp * @brief 增强的响应测试 - 提升覆盖率到 50% - * + * * 目标:提升 uvhttp_response.c 覆盖率从 5.6% 到 50% - * + * * 测试内容: * - uvhttp_response_init * - uvhttp_response_set_status @@ -16,10 +16,11 @@ * - uvhttp_response_send */ +#include "uvhttp_allocator.h" +#include "uvhttp_response.h" + #include #include -#include "uvhttp_response.h" -#include "uvhttp_allocator.h" /* 测试初始化响应 - NULL 参数 */ TEST(UvhttpResponseEnhancedTest, InitNull) { @@ -35,7 +36,8 @@ TEST(UvhttpResponseEnhancedTest, SetStatusNull) { /* 测试设置响应头 - NULL 参数 */ TEST(UvhttpResponseEnhancedTest, SetHeaderNullResponse) { - uvhttp_error_t result = uvhttp_response_set_header(nullptr, "Content-Type", "text/plain"); + uvhttp_error_t result = + uvhttp_response_set_header(nullptr, "Content-Type", "text/plain"); EXPECT_NE(result, UVHTTP_OK); } @@ -43,7 +45,8 @@ TEST(UvhttpResponseEnhancedTest, SetHeaderNullResponse) { TEST(UvhttpResponseEnhancedTest, SetHeaderNullName) { uvhttp_response_t response; memset(&response, 0, sizeof(response)); - uvhttp_error_t result = uvhttp_response_set_header(&response, nullptr, "text/plain"); + uvhttp_error_t result = + uvhttp_response_set_header(&response, nullptr, "text/plain"); EXPECT_NE(result, UVHTTP_OK); } @@ -51,7 +54,8 @@ TEST(UvhttpResponseEnhancedTest, SetHeaderNullName) { TEST(UvhttpResponseEnhancedTest, SetHeaderNullValue) { uvhttp_response_t response; memset(&response, 0, sizeof(response)); - uvhttp_error_t result = uvhttp_response_set_header(&response, "Content-Type", nullptr); + uvhttp_error_t result = + uvhttp_response_set_header(&response, "Content-Type", nullptr); EXPECT_NE(result, UVHTTP_OK); } @@ -93,13 +97,15 @@ TEST(UvhttpResponseEnhancedTest, BuildDataNullResponse) { TEST(UvhttpResponseEnhancedTest, BuildDataNullOutput) { uvhttp_response_t response; memset(&response, 0, sizeof(response)); - uvhttp_error_t result = uvhttp_response_build_data(&response, nullptr, nullptr); + uvhttp_error_t result = + uvhttp_response_build_data(&response, nullptr, nullptr); EXPECT_NE(result, UVHTTP_OK); } /* 测试发送原始数据 - NULL 参数 */ TEST(UvhttpResponseEnhancedTest, SendRawNull) { - uvhttp_error_t result = uvhttp_response_send_raw(nullptr, 0, nullptr, nullptr); + uvhttp_error_t result = + uvhttp_response_send_raw(nullptr, 0, nullptr, nullptr); EXPECT_NE(result, UVHTTP_OK); } @@ -113,7 +119,7 @@ TEST(UvhttpResponseEnhancedTest, SendNull) { TEST(UvhttpResponseEnhancedTest, ResponseFieldInitialization) { uvhttp_response_t response; memset(&response, 0, sizeof(response)); - + EXPECT_EQ(response.client, nullptr); EXPECT_EQ(response.status_code, 0); EXPECT_EQ(response.header_count, 0); @@ -151,7 +157,7 @@ TEST(UvhttpResponseEnhancedTest, MultipleNullCalls) { uvhttp_response_set_body(nullptr, "test", 4); uvhttp_response_cleanup(nullptr); uvhttp_response_free(nullptr); - + char* data = nullptr; size_t length = 0; uvhttp_response_build_data(nullptr, &data, &length); @@ -172,7 +178,7 @@ TEST(UvhttpResponseEnhancedTest, ResponseStructAlignment) { TEST(UvhttpResponseEnhancedTest, ResponseHeadersArray) { uvhttp_response_t response; memset(&response, 0, sizeof(response)); - + EXPECT_LT(response.header_count, MAX_HEADERS); EXPECT_GT(MAX_HEADERS, 0); } @@ -181,24 +187,24 @@ TEST(UvhttpResponseEnhancedTest, ResponseHeadersArray) { TEST(UvhttpResponseEnhancedTest, SetStatusCodes) { uvhttp_response_t response; memset(&response, 0, sizeof(response)); - + // 测试 200 OK uvhttp_error_t result = uvhttp_response_set_status(&response, 200); EXPECT_EQ(result, UVHTTP_OK); EXPECT_EQ(response.status_code, 200); - + // 测试 404 Not Found memset(&response, 0, sizeof(response)); result = uvhttp_response_set_status(&response, 404); EXPECT_EQ(result, UVHTTP_OK); EXPECT_EQ(response.status_code, 404); - + // 测试 500 Internal Server Error memset(&response, 0, sizeof(response)); result = uvhttp_response_set_status(&response, 500); EXPECT_EQ(result, UVHTTP_OK); EXPECT_EQ(response.status_code, 500); - + // 测试无效状态码 memset(&response, 0, sizeof(response)); result = uvhttp_response_set_status(&response, 999); @@ -209,22 +215,24 @@ TEST(UvhttpResponseEnhancedTest, SetStatusCodes) { TEST(UvhttpResponseEnhancedTest, SetHeaders) { uvhttp_response_t response; memset(&response, 0, sizeof(response)); - + // 测试 Content-Type - uvhttp_error_t result = uvhttp_response_set_header(&response, "Content-Type", "text/plain"); + uvhttp_error_t result = + uvhttp_response_set_header(&response, "Content-Type", "text/plain"); EXPECT_EQ(result, UVHTTP_OK); EXPECT_GT(response.header_count, 0); - + // 测试 Content-Length result = uvhttp_response_set_header(&response, "Content-Length", "100"); EXPECT_EQ(result, UVHTTP_OK); - + // 测试 Cache-Control result = uvhttp_response_set_header(&response, "Cache-Control", "no-cache"); EXPECT_EQ(result, UVHTTP_OK); - + // 测试自定义头 - result = uvhttp_response_set_header(&response, "X-Custom-Header", "custom-value"); + result = uvhttp_response_set_header(&response, "X-Custom-Header", + "custom-value"); EXPECT_EQ(result, UVHTTP_OK); // memset 将 headers_capacity 置零,首次 set_header 会分配 headers_extra; @@ -242,18 +250,20 @@ TEST(UvhttpResponseEnhancedTest, SetBodySizes) { EXPECT_EQ(result, UVHTTP_ERROR_INVALID_PARAM); // 测试小响应体 - uvhttp_response_cleanup(&response); // 释放上一次 set_body 的 body(若有) + uvhttp_response_cleanup(&response); // 释放上一次 set_body 的 body(若有) memset(&response, 0, sizeof(response)); result = uvhttp_response_set_body(&response, "test", 4); EXPECT_EQ(result, UVHTTP_OK); EXPECT_EQ(response.body_length, 4); // 测试中等响应体 - uvhttp_response_cleanup(&response); // 释放小响应体的 body,避免 memset 丢失指针 + uvhttp_response_cleanup( + &response); // 释放小响应体的 body,避免 memset 丢失指针 memset(&response, 0, sizeof(response)); char medium_body[1024]; memset(medium_body, 'A', sizeof(medium_body)); - result = uvhttp_response_set_body(&response, medium_body, sizeof(medium_body)); + result = + uvhttp_response_set_body(&response, medium_body, sizeof(medium_body)); EXPECT_EQ(result, UVHTTP_OK); EXPECT_EQ(response.body_length, sizeof(medium_body)); @@ -265,11 +275,12 @@ TEST(UvhttpResponseEnhancedTest, SetBodySizes) { TEST(UvhttpResponseEnhancedTest, SetBodyTooLarge) { uvhttp_response_t response; memset(&response, 0, sizeof(response)); - + // 测试超过限制的响应体 char large_body[MAX_RESPONSE_BODY_LEN + 1]; memset(large_body, 'A', sizeof(large_body)); - uvhttp_error_t result = uvhttp_response_set_body(&response, large_body, sizeof(large_body)); + uvhttp_error_t result = + uvhttp_response_set_body(&response, large_body, sizeof(large_body)); EXPECT_NE(result, UVHTTP_OK); } @@ -277,14 +288,14 @@ TEST(UvhttpResponseEnhancedTest, SetBodyTooLarge) { TEST(UvhttpResponseEnhancedTest, SetHeadersTooMany) { uvhttp_response_t response; memset(&response, 0, sizeof(response)); - + // 添加超过限制的头 for (int i = 0; i < MAX_HEADERS + 10; i++) { char header_name[32]; snprintf(header_name, sizeof(header_name), "X-Header-%d", i); uvhttp_response_set_header(&response, header_name, "value"); } - + // 头数量应该被限制 EXPECT_LE(response.header_count, MAX_HEADERS); @@ -296,28 +307,89 @@ TEST(UvhttpResponseEnhancedTest, SetHeadersTooMany) { TEST(UvhttpResponseEnhancedTest, Http11OptimizationFields) { uvhttp_response_t response; memset(&response, 0, sizeof(response)); - + // 测试 keepalive 字段 response.keepalive = 1; EXPECT_EQ(response.keepalive, 1); - + // 测试 sent 字段 response.sent = 1; EXPECT_EQ(response.sent, 1); - + // 测试 finished 字段 response.finished = 1; EXPECT_EQ(response.finished, 1); - + // 测试 compress 字段 response.compress = 1; EXPECT_EQ(response.compress, 1); - + // 测试 cache_ttl 字段 response.cache_ttl = 3600; EXPECT_EQ(response.cache_ttl, 3600); - + // 测试 cache_expires 字段 response.cache_expires = 1234567890; EXPECT_EQ(response.cache_expires, 1234567890); -} \ No newline at end of file +} +/* ========== set_body 的替换语义:失败不得破坏已有 body ========== */ + +/* + * v2.10:set_body 此前是"先 free 旧 body、再 alloc 新 body"。分配失败时 + * 返回 OUT_OF_MEMORY,而旧值已被释放且 body_length 被清零 —— 调用方拿到 + * 一个空响应,无法回退,也没有重试的素材。 + * + * 现在改为 allocate-then-swap(set_header 靠 realloc 免费得到同样的性质)。 + * 下面锁住**可达**的失败路径:参数非法时旧 body 必须原封不动。OOM 分支 + * 需要分配失败注入才能触发,本仓暂无该设施(test_lru_cache_full_coverage + * 也有同样的说明),故未覆盖 —— 修复本身按构造成立,不靠测试兜底。 + */ + +/* 非法参数(空 body / 长度 0 / 超长)不得动已有 body */ +TEST(UvhttpResponseEnhancedTest, FailedSetBodyKeepsPreviousBody) { + uvhttp_response_t response; + memset(&response, 0, sizeof(response)); + + const char* original = "original payload"; + ASSERT_EQ(uvhttp_response_set_body(&response, original, strlen(original)), + UVHTTP_OK); + char* body_before = response.body; + + EXPECT_EQ(uvhttp_response_set_body(&response, nullptr, 8), + UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ(uvhttp_response_set_body(&response, "", 0), + UVHTTP_ERROR_INVALID_PARAM); + EXPECT_EQ(uvhttp_response_set_body(&response, "x", + (size_t)UVHTTP_MAX_BODY_SIZE + 1), + UVHTTP_ERROR_INVALID_PARAM); + + EXPECT_EQ(response.body, body_before) << "失败的 set_body 换了 body 指针"; + EXPECT_EQ(response.body_length, strlen(original)) + << "失败的 set_body 改了 body_length"; + /* body 不带 NUL 终止符(库内一律按 body_length + memcpy 使用),因此必须 + * 按长度比较 —— 用 EXPECT_STREQ 会读到分配块之外(ASan 会判越界)。 */ + ASSERT_EQ(response.body_length, strlen(original)); + EXPECT_EQ(memcmp(response.body, original, strlen(original)), 0) + << "旧 body 内容必须保持不变"; + + uvhttp_response_cleanup(&response); +} + +/* 二次设置是替换而非追加:旧内容不得残留。 + * 这里刻意不断言 body 指针换了新块 —— 分配器完全可能把刚释放的块原样复用, + * 那种断言测的是分配器而不是语义(旧实现同样会过/红,与本用例无关)。 */ +TEST(UvhttpResponseEnhancedTest, SecondSetBodyReplacesFirst) { + uvhttp_response_t response; + memset(&response, 0, sizeof(response)); + + ASSERT_EQ(uvhttp_response_set_body(&response, "first", 5), UVHTTP_OK); + ASSERT_EQ(uvhttp_response_set_body(&response, "second-value", 12), + UVHTTP_OK); + + EXPECT_EQ(response.body_length, 12u); + ASSERT_EQ(response.body_length, 12u); + EXPECT_EQ(memcmp(response.body, "second-value", 12), 0) + << "第二次 set_body 必须整体替换,不得保留旧内容"; + + uvhttp_response_cleanup(&response); +}