From ee7fbe3c4d1f23f8bc0fef41f9f96483e4659da9 Mon Sep 17 00:00:00 2001 From: =?utf8?q?=E5=AD=99=E6=98=A5=E9=98=B3?= Date: Tue, 6 Jan 2026 18:33:00 +0800 Subject: [PATCH] =?utf8?q?=E4=B8=8B=E8=BD=BD=E6=B7=BB=E5=8A=A0md5sum?= =?utf8?q?=E6=94=AF=E6=8C=81=EF=BC=8Cmd5=E4=B8=8D=E7=AC=A6=E5=90=88?= =?utf8?q?=E7=9A=84=E6=97=B6=E5=80=99=EF=BC=8C=E4=BC=9A=E9=87=8D=E6=96=B0?= =?utf8?q?=E4=B8=8B=E8=BD=BD=E6=96=87=E4=BB=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- .../sdk/zhihui/leiniao/imsl_ln/ln_api_emoji.c | 243 ++++++++++++++++-- 1 file changed, 222 insertions(+), 21 deletions(-) diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_emoji.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_emoji.c index 61d301a9e..975b9e625 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_emoji.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_emoji.c @@ -10,6 +10,10 @@ #include #include "custom_utils.h" #include +#include "mbedtls/md5.h" +#ifdef SUPPORT_CAT1_SERVICE +#include "cat1_pub_api.h" +#endif /* 宏定义 */ #define MAX_PATH_LEN 512 // 最大路径长度 @@ -20,6 +24,7 @@ typedef struct char *emotion; // 心情类型(动态分配) char *emojiUrl; // 表情文件URL(动态分配) long long size; // 表情文件大小(字节,支持断点续传判断;0 表示未知) + char *md5; // MD5校验值(动态分配,可为NULL) } EmotionEmoji; /* 表情包配置结构体 */ @@ -40,6 +45,9 @@ static volatile int g_emoji_download_cancelled = 0; /* 取消下载标志 */ /* 前置声明,便于在本节工具函数中调用 */ static int ln_emoji_is_cached(const EmojiPackage *pkg, const EmotionEmoji *emoji); static void ln_gen_emoji_cache_path(const EmojiPackage *pkg, const EmotionEmoji *emoji, char *path, size_t path_len); +/* MD5计算和校验函数 */ +static int ln_calc_file_md5_lower_hex(const char *file_path, char out_hex[33]); +static int ln_emoji_verify_md5(const char *file_path, const char *expected_md5); /* 毫秒时间戳 */ static inline unsigned long long ln_now_ms(void) { @@ -153,6 +161,11 @@ static void ln_free_emoji_list() free(g_emoji_package.emojis[i].emojiUrl); g_emoji_package.emojis[i].emojiUrl = NULL; } + if (g_emoji_package.emojis[i].md5) + { + free(g_emoji_package.emojis[i].md5); + g_emoji_package.emojis[i].md5 = NULL; + } } free(g_emoji_package.emojis); g_emoji_package.emojis = NULL; @@ -413,13 +426,103 @@ static void ln_gen_emoji_cache_path(const EmojiPackage *pkg, const EmotionEmoji WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 生成表情缓存路径成功,path=%s", path); } +/* 计算文件的MD5值(小写十六进制字符串) */ +static int ln_calc_file_md5_lower_hex(const char *file_path, char out_hex[33]) +{ + if (!file_path || !out_hex) return -1; + FILE *fp = fopen(file_path, "rb"); + if (!fp) return -1; + mbedtls_md5_context ctx; + mbedtls_md5_init(&ctx); + if (mbedtls_md5_starts(&ctx) != 0) { mbedtls_md5_free(&ctx); fclose(fp); return -1; } + unsigned char buf[1024]; + size_t n; + while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) { + if (mbedtls_md5_update(&ctx, buf, n) != 0) { mbedtls_md5_free(&ctx); fclose(fp); return -1; } + } + fclose(fp); + unsigned char md5_ctx_buf[16]; + if (mbedtls_md5_finish(&ctx, md5_ctx_buf) != 0) { mbedtls_md5_free(&ctx); return -1; } + mbedtls_md5_free(&ctx); + static const char hex[] = "0123456789abcdef"; + for (int i = 0; i < 16; i++) { + out_hex[i * 2] = hex[(md5_ctx_buf[i] >> 4) & 0x0F]; + out_hex[i * 2 + 1] = hex[md5_ctx_buf[i] & 0x0F]; + } + out_hex[32] = '\0'; + return 0; +} + +/* 校验文件的MD5值 */ +static int ln_emoji_verify_md5(const char *file_path, const char *expected_md5) +{ + if (file_path == NULL || expected_md5 == NULL || expected_md5[0] == '\0') { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_EMOJI] md5 verify: invalid args"); + return -1; + } + char calc[33] = {0}; + if (ln_calc_file_md5_lower_hex(file_path, calc) != 0) { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_EMOJI] md5 verify: calc failed for %s", file_path); + return -1; + } + char expect_lower[33] = {0}; + size_t elen = strlen(expected_md5); + size_t copy_len = elen > 32 ? 32 : elen; + for (size_t i = 0; i < copy_len; i++) { + char ch = expected_md5[i]; + if (ch >= 'A' && ch <= 'Z') ch = (char)(ch - 'A' + 'a'); + expect_lower[i] = ch; + } + expect_lower[32] = '\0'; + int ok = (strcmp(calc, expect_lower) == 0) ? 1 : 0; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD_EMOJI] md5 verify result=%s, calc=%s, expect=%s", + ok ? "OK" : "FAIL", calc, expect_lower); + return ok; +} + /* 工具函数:检查表情是否已缓存 */ static int ln_emoji_is_cached(const EmojiPackage *pkg, const EmotionEmoji *emoji) { char path[MAX_PATH_LEN] = {0}; ln_gen_emoji_cache_path(pkg, emoji, path, sizeof(path)); struct stat st; - return (stat(path, &st) == 0 && S_ISREG(st.st_mode)) ? 1 : 0; + + /* 检查文件是否存在,同时获取文件大小(stat是元数据操作,很快) */ + if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) { + return 0; + } + + /* 优先检查文件大小(使用已获取的st.st_size,避免额外IO) + * 如果大小不匹配,直接返回,避免读取整个文件计算MD5 */ + if (emoji->size > 0) { + unsigned long long file_size = (unsigned long long)st.st_size; + if (file_size != (unsigned long long)emoji->size) { + /* 文件大小不匹配,可能不完整,删除文件并返回0以触发重新下载 */ + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 文件大小不匹配(期望:%lld, 实际:%llu),删除不完整文件: %s", + emoji->emotion ? emoji->emotion : "unknown", emoji->size, file_size, path); + unlink(path); + return 0; + } + } + + /* 文件大小匹配后,再进行MD5校验(需要读取整个文件) */ + if (emoji->md5 != NULL && emoji->md5[0] != '\0') { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 检查已缓存文件 %s 的MD5,期望MD5=%s", + emoji->emotion ? emoji->emotion : "unknown", emoji->md5); + int md5_ok = ln_emoji_verify_md5(path, emoji->md5); + if (md5_ok != 1) { + /* MD5校验失败,文件可能不完整,删除文件并返回0以触发重新下载 */ + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s MD5校验失败,删除不完整文件: %s", + emoji->emotion ? emoji->emotion : "unknown", path); + unlink(path); + return 0; + } else { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 已缓存文件MD5校验通过", + emoji->emotion ? emoji->emotion : "unknown"); + } + } + + return 1; } /* 批量下载表情并上报结果 */ @@ -492,12 +595,16 @@ static void ln_emoji_download(void *arg) } } downloaded_count = cached_count; - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 初始统计已下载数量,downloaded_count=%d", downloaded_count); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 初始统计已下载数量,downloaded_count=%d/%d", downloaded_count, pkg->emojiCount); if (pkg->emojiCount > 0) { int init_progress = (int)(((double)downloaded_count / pkg->emojiCount) * 100); + // 确保进度在0-100范围内 + if (init_progress < 0) init_progress = 0; + if (init_progress > 100) init_progress = 100; if (init_progress > 0) { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 初始进度=%d%%", init_progress); push_download_progress_notification(init_progress); } } @@ -547,33 +654,108 @@ static void ln_emoji_download(void *arg) break; } - if (uapi_ln_download_file_resume_with_retry(pkg->emojis[i].emojiUrl, save_path, 10) != ERRCODE_SUCC) + int download_success = 0; + int max_retry = 2; // 最大重试次数(包括MD5校验失败后的重试),减少重试次数以加快失败处理 + int retry_count = 0; + + while (retry_count < max_retry) { - // 检查是否因为取消而失败 - if (g_emoji_download_cancelled) + // 如果是重试,先删除旧文件 + if (retry_count > 0) { - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 下载被取消"); - all_success = 0; - break; + unlink(save_path); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 第%d次重试下载", pkg->emojis[i].emotion, retry_count); } + + // 减少内部重试次数,从10次减少到3次,避免单个文件耗时过长 + if (uapi_ln_download_file_resume_with_retry(pkg->emojis[i].emojiUrl, save_path, 3) != ERRCODE_SUCC) + { + // 检查是否因为取消而失败 + if (g_emoji_download_cancelled) + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] 下载被取消"); + all_success = 0; + break; + } - all_success = 0; - failed_count++; - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载失败", pkg->emojis[i].emotion); - // 上报下载失败状态 - char fail_msg[128]; - snprintf(fail_msg, sizeof(fail_msg), "表情 %s 下载失败", pkg->emojis[i].emotion); - ln_ws_send_emoji_download_status_report(pkg->characterId, pkg->eyesColor, pkg->eyesShape, "download_fail", -1, fail_msg); - /* 单个文件三次重试仍失败,终止后续下载并尽快上报失败 */ + retry_count++; + if (retry_count >= max_retry) + { + // 单个文件重试次数已用完,立即跳过该文件,继续下载下一个文件 + all_success = 0; + failed_count++; + // 删除可能存在的部分下载文件,避免留下不完整的文件 + unlink(save_path); + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载失败(已重试%d次),删除不完整文件,跳过该文件,继续下载下一个", pkg->emojis[i].emotion, retry_count); + // 上报下载失败状态 + char fail_msg[128]; + snprintf(fail_msg, sizeof(fail_msg), "表情 %s 下载失败", pkg->emojis[i].emotion); + ln_ws_send_emoji_download_status_report(pkg->characterId, pkg->eyesColor, pkg->eyesShape, "download_fail", -1, fail_msg); + // 跳过该文件,继续下载下一个文件(不终止整个下载任务) + break; // 退出while循环,继续for循环的下一个文件 + } + // 重试前等待一段时间,避免频繁重试 + sleep(2); + continue; // 继续重试 + } + + // 下载成功,进行MD5校验(如果有MD5值) + if (pkg->emojis[i].md5 && pkg->emojis[i].md5[0] != '\0') + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载完成,开始MD5校验,期望MD5=%s", + pkg->emojis[i].emotion, pkg->emojis[i].md5); + + if (ln_emoji_verify_md5(save_path, pkg->emojis[i].md5) != 1) + { + // MD5校验失败,删除文件并重试 + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s MD5校验失败,删除文件并重试", pkg->emojis[i].emotion); + unlink(save_path); + retry_count++; + if (retry_count >= max_retry) + { + // MD5校验失败且重试次数已用完,立即跳过该文件,继续下载下一个文件 + all_success = 0; + failed_count++; + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s MD5校验失败(已重试%d次),跳过该文件,继续下载下一个", pkg->emojis[i].emotion, retry_count); + char fail_msg[128]; + snprintf(fail_msg, sizeof(fail_msg), "表情 %s MD5校验失败", pkg->emojis[i].emotion); + ln_ws_send_emoji_download_status_report(pkg->characterId, pkg->eyesColor, pkg->eyesShape, "download_fail", -1, fail_msg); + break; // 退出while循环,继续for循环的下一个文件 + } + // 重试前等待一段时间,避免频繁重试 + sleep(2); + continue; // 继续重试 + } + else + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s MD5校验通过", pkg->emojis[i].emotion); + } + } + else + { + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载完成(无MD5校验值,跳过校验)", pkg->emojis[i].emotion); + } + + // 下载和校验都成功 + download_success = 1; break; } - else + + // 处理下载结果(成功或失败) + if (download_success) { downloaded_count++; /* 表情进度上报,按下载成功表情数量计算 */ int progress = (int)(((double)downloaded_count / pkg->emojiCount) * 100); + // 确保进度在0-100范围内 + if (progress < 0) progress = 0; + if (progress > 100) progress = 100; + + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载并校验完成, downloaded_count=%d/%d, progress=%d%%", + pkg->emojis[i].emotion, downloaded_count, pkg->emojiCount, progress); + + // 更新UI进度 push_download_progress_notification(progress); - WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[DOWNLOAD] emoji %s 下载完成", pkg->emojis[i].emotion); // 每次进度更新都同步发送协议给服务器更新进度 char progress_msg[64]; @@ -769,7 +951,11 @@ void ln_start_emoji_download(void) // 反初始化HTTP文件下载服务 http_file_download_deinit(); } - +/** + * 带md5sum的参考格式 + * 解析时要注意带md5和不带md5的要兼容。 + * {"deviceId":"867998086628226","timestamp":1767672001,"messageId":"18692b900b384a40","command":"DownloadEmoji","data":{"characterId":"de50ca21420f","eyesColor":"brown","eyesShape":"calm-eyes","emotionEmojis":[{"emotion":"happy","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Smile.mp4","size":715594,"md5":"262d7d33f273c0aafa137e2d72d2eaf9"},{"emotion":"laughing","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Laugh.mp4","size":773624,"md5":"7d74df7f75661452ce3d1e51e2bd3d58"},{"emotion":"angry","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Angry.mp4","size":754492,"md5":"b4e6f8ac6c0d20c1e71336ca10d82ffa"},{"emotion":"wronged","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Wronged.mp4","size":1169130,"md5":"62f651821a12b224bd0b9e4d61ad16d8"},{"emotion":"heartbeat","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Heartbeat.mp4","size":940392,"md5":"2e4df95c116e82f4ab31ed21b6d7fc49"},{"emotion":"awkward","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Embarrassed.mp4","size":793970,"md5":"e168cc2e789732c61653c91cb72a3a27"},{"emotion":"confused","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Doubt.mp4","size":812766,"md5":"70c2c027e261394c78dbaaff5284829c"},{"emotion":"shocked","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Shocked.mp4","size":843018,"md5":"decc6a03f7d1356a6f3f82f57636bf0a"},{"emotion":"crying","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Tears.mp4","size":898970,"md5":"369e17f42842db9c0b1a330800416760"},{"emotion":"blowing-kiss","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Kiss.mp4","size":539583,"md5":"d1dc573d076a59ffae8fbdbb9eceb432"},{"emotion":"sleepy","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Sleepy.mp4","size":661627,"md5":"2483cc4a1aa57b9b15b8ab3d17bf453b"},{"emotion":"mischievous","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Naughty.mp4","size":636318,"md5":"efe8d0d6d10bebf32cfe10689cba8702"},{"emotion":"speechless","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Speechless.mp4","size":875623,"md5":"a58d5ba63375732a8259f294dfe35a92"},{"emotion":"dizzy","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Dizzy.mp4","size":1133467,"md5":"7f6988e7509655d27ac4a0e53859930b"},{"emotion":"thinking","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Think.mp4","size":960243,"md5":"b09dc26e2a9b1ece3609bb5c019c5619"},{"emotion":"working","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/WorkAndStudy.mp4","size":1029099,"md5":"bc483c6728b3767cc1bd5dff7a1a2411"},{"emotion":"exercising","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Exercise.mp4","size":1056919,"md5":"65382bf4fc5a357dcc963d4dc4495ec8"},{"emotion":"playing","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Play.mp4","size":1077517,"md5":"d1a379980e2c144b02c915b41711e62f"},{"emotion":"daydreaming","emojiUrl":"https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Brown/Daze.mp4","size":1034574,"md5":"f71e5d96ba3afff009ff70ef0f39ebe8"}],"size":16706926}} + */ /* 解析下载表情命令JSON */ void ln_parse_download_emoji(const char *json_buf, int json_len) { @@ -878,9 +1064,11 @@ void ln_parse_download_emoji(const char *json_buf, int json_len) cJSON *emotion = cJSON_GetObjectItem(item, "emotion"); cJSON *emojiUrl = cJSON_GetObjectItem(item, "emojiUrl"); cJSON *size_node = cJSON_GetObjectItem(item, "size"); + cJSON *md5_node = cJSON_GetObjectItem(item, "md5"); g_emoji_package.emojis[i].emotion = ln_safe_strdup((emotion && cJSON_IsString(emotion)) ? emotion->valuestring : ""); g_emoji_package.emojis[i].emojiUrl = ln_safe_strdup((emojiUrl && cJSON_IsString(emojiUrl)) ? emojiUrl->valuestring : ""); + /* 解析单个表情大小(字节) */ long long emoji_size = 0; if (size_node) @@ -895,11 +1083,24 @@ void ln_parse_download_emoji(const char *json_buf, int json_len) } } g_emoji_package.emojis[i].size = (emoji_size > 0) ? emoji_size : 0; - WEARABLE_LOGD(WEARABLE_LOG_MODULE_APP, " emoji %d: emotion=%s, url=%s, size=%lld", + + /* 解析MD5校验值 */ + if (md5_node && cJSON_IsString(md5_node) && md5_node->valuestring) + { + g_emoji_package.emojis[i].md5 = ln_safe_strdup(md5_node->valuestring); + } + else + { + g_emoji_package.emojis[i].md5 = NULL; + } + + /* 打印解析到的size和md5信息 */ + WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, " emoji %d: emotion=%s, url=%s, size=%lld, md5=%s", i + 1, g_emoji_package.emojis[i].emotion, g_emoji_package.emojis[i].emojiUrl, - g_emoji_package.emojis[i].size); + g_emoji_package.emojis[i].size, + g_emoji_package.emojis[i].md5 ? g_emoji_package.emojis[i].md5 : "NULL"); } } } -- 2.17.1