fix(ncm): tag 嵌入按实际容器拒绝 MP4/M4A(ID3v2 前缀会破坏 MP4 容器)

download 扩展名在 type 非 flac 时一律落 mp3;若服务端降级返回 MP4/M4A("....ftyp"),
给文件加 ID3v2 头会破坏容器。ncm_tag_embed_file 现读 12 字节 magic:fLaC → VORBIS_COMMENT,
"ftyp" → NCM_ERR_ARG 拒绝(调用方 commit 裸流),其余按 MP3 处理。
test_ncm_tag 增 2 条断言(拒绝 + 原文件不变)→ 53 PASS / 0 FAIL,0 warning。
This commit is contained in:
AskaEth committed 2026-11-04 20:49:25 +08:00
1 parent 39465ea1ef
commit a9c94c7221
2 files changed
+14 -3

No files matched your search

+7 -3
View File
@@ -561,9 +561,10 @@ int ncm_tag_embed_file(const char *path, const char *ext, const ncm_tag_meta_t *
{ {
char tmp[NCM_TAG_PATH_MAX]; char tmp[NCM_TAG_PATH_MAX];
struct stat st; struct stat st;
unsigned char magic[4]; unsigned char magic[12];
unsigned char *buf; unsigned char *buf;
int in, out, rc, is_flac, n; int in, out, rc, is_flac, n;
ssize_t mgot;
if (!path || !path[0] || !m) return NCM_ERR_ARG; if (!path || !path[0] || !m) return NCM_ERR_ARG;
if (!m->title[0] && !m->artist[0] && !m->album[0]) return NCM_OK; /* 无元数据:不动文件 */ if (!m->title[0] && !m->artist[0] && !m->album[0]) return NCM_OK; /* 无元数据:不动文件 */
@@ -579,12 +580,15 @@ int ncm_tag_embed_file(const char *path, const char *ext, const ncm_tag_meta_t *
if (in < 0) return NCM_ERR_IO; if (in < 0) return NCM_ERR_IO;
if (fstat(in, &st) != 0 || st.st_size <= 0) { close(in); return NCM_ERR_IO; } if (fstat(in, &st) != 0 || st.st_size <= 0) { close(in); return NCM_ERR_IO; }
if (read_full(in, magic, sizeof magic) != (ssize_t)sizeof magic) { close(in); return NCM_ERR_IO; } mgot = read_full(in, magic, sizeof magic);
if (mgot < 4) { close(in); return NCM_ERR_IO; }
if (lseek(in, 0, SEEK_SET) < 0) { close(in); return NCM_ERR_IO; } if (lseek(in, 0, SEEK_SET) < 0) { close(in); return NCM_ERR_IO; }
is_flac = (memcmp(magic, "fLaC", 4) == 0); is_flac = (memcmp(magic, "fLaC", 4) == 0);
/* 声明 flac 却无 fLaC 魔数 = 残体/损坏:拒绝写(调用方 commit 裸流降级)。 /* 声明 flac 却无 fLaC 魔数 = 残体/损坏:拒绝写(调用方 commit 裸流降级)。
* 声明 mp3 而内容实为 FLAC:按容器写 VORBIS_COMMENT(真机降级产物同理)。 */ * 声明 mp3 而内容实为 FLAC:按容器写 VORBIS_COMMENT(真机降级产物同理)。
* MP4/M4A(第 4..7 字节 "ftyp"):ID3v2 头会破坏 MP4 容器 → 拒绝(裸流降级)。 */
if (!is_flac && ext && strcasecmp(ext, "flac") == 0) { close(in); return NCM_ERR_ARG; } if (!is_flac && ext && strcasecmp(ext, "flac") == 0) { close(in); return NCM_ERR_ARG; }
if (!is_flac && mgot >= 8 && memcmp(magic + 4, "ftyp", 4) == 0) { close(in); return NCM_ERR_ARG; }
buf = (unsigned char *)malloc(NCM_TAG_CHUNK); buf = (unsigned char *)malloc(NCM_TAG_CHUNK);
if (!buf) { close(in); return NCM_ERR_OOM; } if (!buf) { close(in); return NCM_ERR_OOM; }
+7
View File
@@ -347,6 +347,13 @@ static void test_edges(void)
snprintf(tmp, sizeof tmp, "%s.tag.tmp", path); snprintf(tmp, sizeof tmp, "%s.tag.tmp", path);
CHECK(stat(tmp, &st) != 0, "no .tag.tmp left behind"); CHECK(stat(tmp, &st) != 0, "no .tag.tmp left behind");
} }
/* MP4/M4A("....ftyp")不是 mp3:ID3v2 前缀会破坏 MP4 容器 → 必须拒绝(降级裸流) */
snprintf(path, sizeof path, TMPDIR_PREFIX "/fake.m4a.mp3");
file_put(path, "\x00\x00\x00\x20""ftypisom\x00\x00\x00\x00", 16);
CHECK(ncm_tag_embed_file(path, "mp3", &m) != 0, "MP4 container rejected (no ID3 prefix)");
before = file_slurp(path, &blen);
CHECK(blen == 16 && memcmp(before + 4, "ftyp", 4) == 0, "MP4 file unchanged");
free(before);
/* 不存在的文件 */ /* 不存在的文件 */
CHECK(ncm_tag_embed_file(TMPDIR_PREFIX "/nope.mp3", "mp3", &m) != 0, "missing file -> error"); CHECK(ncm_tag_embed_file(TMPDIR_PREFIX "/nope.mp3", "mp3", &m) != 0, "missing file -> error");
CHECK(ncm_tag_read_file(TMPDIR_PREFIX "/nope.mp3", &m) == NCM_ERR_NOENT, CHECK(ncm_tag_read_file(TMPDIR_PREFIX "/nope.mp3", &m) == NCM_ERR_NOENT,