一个文件名以 .pdf 结尾,浏览器给出的类型也是 application/pdf,界面于是显示“已验证 PDF”。这一步推断并不成立:文件名与类型声明可以一致,但它们还不是文件内容的验证结果。
以米米商聊为背景,已整理的手机端资料包含文件消息。本文借这一功能讨论浏览器文件元数据的展示与检查,不断言产品存在下述问题,也不推断产品允许上传的类型、预览方式或服务端实现。所有文件名和类型均为独立合成数据,没有选择、读取或上传真实用户文件。
1. 后缀、类型声明与实际内容来自不同层次
文件名后缀是一段名称文本。MIME 媒体类型则是对内容类型的声明,例如 application/pdf、image/png。二者可以用于组织图标、显示提示或发现明显不一致,但不能自动变成内容已经正确的证明。
浏览器中的 File 继承了 Blob 的 type 属性。根据 MDN 的 Blob.type 文档,当前浏览器实现通常不会读取文件字节流来确定这个属性,而可能依据后缀及客户端配置推断;无法判断时也可能返回空字符串。
因此,后缀与 file.type 未必是两份彼此独立的证据。若类型提示本身就来自后缀,它们相互符合,只能说明声明没有在这一层表现出冲突。
| 信息 | 能提供的线索 | 不能直接证明的事情 |
|---|---|---|
| 文件名末尾的后缀 | 名称声称的类型 | 字节内容确实属于该格式 |
浏览器 type |
客户端给出的类型提示 | 已经过内容解析或安全检查 |
| 服务端接收的类型声明 | 请求携带的声明 | 声明来自可信的内容检测 |
| 实际内容解析结果 | 对相应格式的内容检查结果 | 可以替代所有业务约束 |
具体要接受、存储或预览什么,仍需按业务设计内容检查。不能让一个展示图标承担这些职责。

2. 先明确后缀提取与类型提示的处理规则
本例只处理单个文件名,不接收目录路径。取最后一个普通英文句点之后的部分作为后缀,并转为小写;以句点开头但没有后续句点的名称,例如 .env,在本例中视为没有后缀。名称末尾是句点时也不产生后缀。
这意味着 报告.pdf.exe 的后缀是 exe,不是中间的 pdf。而 报告.exe.pdf 的末尾仍是 pdf;这依然只说明名称的声明,不能据此判断真实内容。
文件名不自动去掉尾部空格,也不把全角句点替换成普通句点。规则不一致时,前端、服务端或存储层可能对同一个名称产生不同理解。这里选择保留输入含义,不悄悄重命名。
媒体类型提示取第一个分号之前的部分,去掉首尾空白并转成小写。例如 text/plain; charset=UTF-8 用于本例比较的部分是 text/plain。媒体类型与参数的区别可参考 MDN 的 MIME 类型说明。本例不解析或验证参数,也不是通用 HTTP 头解析器。
3. 一个只比较元数据声明的函数
函数返回的状态故意叫“提示一致”或“提示冲突”,而不是“文件合法”。映射表只有少数演示类型,不代表完整类型注册表或产品允许列表。
function inspectAttachmentMetadata(input) {
if (input === null || typeof input !== "object" || Array.isArray(input)) {
throw new TypeError("metadata must be an object");
}
const {
name, type = "" } = input;
const controls = /[\u0000-\u001f\u007f]/;
if (typeof name !== "string" || name.length < 1 || name.length > 255
|| controls.test(name) || /[\\/]/.test(name)) {
throw new TypeError("invalid example filename");
}
if (typeof type !== "string" || type.length > 128 || controls.test(type)) {
throw new TypeError("invalid type hint");
}
const dot = name.lastIndexOf(".");
const extension = dot > 0 && dot < name.length - 1
? name.slice(dot + 1).toLowerCase() : "";
const mediaType = type.split(";", 1)[0].trim().toLowerCase();
const expected = new Map([
["pdf", "application/pdf"], ["png", "image/png"],
["jpg", "image/jpeg"], ["jpeg", "image/jpeg"], ["txt", "text/plain"]
]);
let status;
if (extension === "") {
status = "missing-extension";
} else if (!expected.has(extension)) {
status = "unmapped-extension";
} else if (mediaType === "" || mediaType === "application/octet-stream") {
status = "insufficient-hint";
} else {
status = expected.get(extension) === mediaType
? "matching-hints" : "conflicting-hints";
}
return {
extension, mediaType, status };
}
名称最多 255 个 UTF-16 代码单元、类型提示最多 128 个代码单元,都是教学输入约束,不是文件系统或产品限制。路径分隔符与部分控制字符被拒绝,也不意味着它已经成为完整的文件名安全处理器。
application/octet-stream 在本例中作为通用二进制提示处理,不被当成某一种具体格式的确认。缺少后缀、映射表不认识后缀、类型提示不足和两种提示冲突分别返回不同状态,避免用一个布尔值混淆所有原因。
函数只比较归一化后的提示字符串,没有检查所有 MIME 语法,也不读取文件字节。matching-hints 最多可以展示为“声明一致,内容未校验”,不能直接映射成“安全文件”或“允许预览”。
4. 用双后缀、空提示与类型冲突检查输出
将下面演示与上一段放在同一个 JavaScript 文件中执行。输入只有合成元数据,不会创建或执行这些文件。
const samples = [
{
name: "报告.PDF", type: "application/pdf" },
{
name: "说明.txt", type: "application/pdf" },
{
name: "图片.png", type: "" },
{
name: "报告.pdf.exe", type: "application/pdf" },
{
name: ".env", type: "text/plain" }
];
const results = samples.map(inspectAttachmentMetadata);
console.assert(results[0].status === "matching-hints");
console.assert(results[1].status === "conflicting-hints");
console.assert(results[2].status === "insufficient-hint");
console.assert(results[3].status === "unmapped-extension");
console.assert(results[4].status === "missing-extension");
console.log(results);
第一项匹配,只能说明 .pdf 与 application/pdf 符合本例映射。第三项信息不足,不代表图片一定损坏。第四项没有命中映射,也不代表函数已经识别出了它的实际内容。
配套验证覆盖大小写、参数提示、缺失类型、双后缀、隐藏名称、尾部空格、全角句点、非法输入和映射组合矩阵。所有测试针对独立元数据函数,没有读取真实文件,也没有执行内容解析、上传服务或产品客户端验收。
5. 两种声明一致,为什么仍然需要内容验证
一个直接的反例是:两份真实内容不同的文件,如果提供相同的 name 与 type,这个函数就会得到同样的结果。它根本没有接收或读取字节内容,自然无法分辨这种差异。
上传请求中的 Content-Type 也不能直接当成可信检测结果。OWASP 文件上传说明指出,客户端提供的类型声明可以被更改;类型声明和文件签名检查也不能单独替代完整的验证策略。
实际接收或预览时,应根据业务允许的格式,交给相应的服务端处理与内容解析流程。这里只保留边界:元数据检查可以帮助发现声明问题,实际内容检查负责验证内容;二者的结果应分别记录,不能把客户端提示直接升级为服务端结论。
| 本例状态 | 可以表达的界面提示 | 不能据此自动决定 |
|---|---|---|
matching-hints |
声明一致,内容未校验 | 文件已安全验证、允许执行或预览 |
conflicting-hints |
名称后缀与类型提示不一致 | 自动修改后缀即可修复内容 |
insufficient-hint |
类型提示不足 | 实际内容必定无效 |
unmapped-extension |
本例未覆盖该后缀 | 该格式在业务中必定禁止 |
missing-extension |
名称没有本例识别的后缀 | 内容无法被任何解析器识别 |
展示文案应让用户知道当前结论来自哪一层。若服务端已经完成某项检查,还应让界面引用该检查结果,而不是重新根据文件名猜测。
6. 把声明检查与业务结论分别验收
接入时先确认输入究竟来自浏览器文件对象、消息元数据还是服务端检测结果,再决定它能支撑哪种提示。同样叫 type 的字段,不一定具有相同来源和可信度。
随后分别检查后缀提取、类型归一化、声明冲突和实际内容处理。文件名显示、存储命名以及预览组件有各自的职责,不能因为一个元数据函数返回匹配,就跳过后续业务流程。
验收不只看正常的 .pdf 示例,还应覆盖空类型、未知映射、双后缀以及相同元数据对应不同内容的情况。对“没有证据”与“证据冲突”给出不同结果,能够减少误报,也避免把图标和安全结论混为一谈。
文件名和 MIME 都是有用的线索,但线索的一致性并不等于内容已经验证。把返回状态、展示文案与证据来源对应起来,才能让文件消息的类型提示准确表达当前已知的信息。
本文由 AI 辅助整理,独立示例经过本地合成验证。产品背景只引用已整理的手机端文件消息资料;映射表和工程建议不代表产品上传规则、预览能力或内部实现。