mime4cj 使用指南:仓颉语言 MIME 类型解析库(getType / getExtension / define)实战与源码剖析
mime4cj 使用指南:仓颉语言 MIME 类型解析库(getType / getExtension / define)实战与源码剖析
mime4cj 是面向仓颉(Cangjie)语言实现的 MIME 类型信息解析库,内置 800+ 标准与非官方 MIME 类型、1,000+ 扩展名映射,提供 MIME.getType()、MIME.getExtension()、MIME.getAllExtensions() 与可自定义的 Mime 对象、define() 映射扩展能力,全程零第三方依赖。读完本文你将掌握 mime4cj 的编译接入方式、四大核心 API 的完整用法与边界行为,并通过源码与测试用例理解其内部映射表、大小写规范化、* 前缀扩展名与 force 覆盖机制的设计原理。
项目定位与核心特性
mime4cj 是一个"用于 MIME 类型信息的 API",即通过文件路径 / 扩展名查询 MIME 类型,或通过 MIME 类型反查默认扩展名与全部扩展名。项目特性明确(见 README.md):
- 覆盖全:支持所有
mime-db类型,完整版内置 800+ MIME 类型、1,000+ 扩展名; - 零依赖:库本身无任何额外依赖,开箱即用。
在 Web 服务开发中,它最常见的应用场景是:根据上传文件的后缀生成 HTTP Content-Type 响应头,或解析 Content-Type 头(含 charset 参数)还原文件扩展名。
架构与源码目录
仓库顶层目录结构如下:
.
├── README.md
├── doc
├── test
└── src
doc:存放库的设计文档、提案与使用文档(含完整的 API 说明);test:存放用例,分为HLT(高层测试)与LLT(底层/库级测试)两组;src:库源码目录,核心实现集中在 src/mime.cj,数据文件为 src/standard.cj(标准类型)与 src/other.cj(非官方类型)。
快速开始:编译与构建
在 Linux / Windows 下,安装好仓颉工具链后,在仓库根目录执行:
cjpm build
构建配置由 cjpm.toml 管理,关键字段如下:
| 字段 | 值 | 说明 |
|---|---|---|
name |
mime |
包名 |
version |
1.0.4 |
库版本号 |
cjc-version |
1.1.3 |
依赖的 cjc 编译器版本 |
output-type |
dynamic |
动态库输出 |
override-compile-option |
-O2 |
覆盖编译优化选项 |
dependencies |
(空) | 印证"无额外依赖"特性 |
[profile] 段还提供了 debug(-g -Woff all)与 release(--fast-math -O2 -Woff all)两套编译剖面选项。目标平台覆盖 aarch64-linux-ohos、x86_64-linux-ohos 与 x86_64-unknown-windows-gnu。
核心 API 一览
完整的接口签名见 doc/feature_api.md,核心类型与全局对象为:
MIME // 不可更改的全局全量 MIME 对象(标准类型 + 非官方类型)
MIME_LITE // 不可更改的全局精简 MIME 对象(仅标准类型)
// mime 信息 type-extensions 键值对
public type TypeMap = HashMap<String, Array<String>>
// 可自定义的 MIME 对象
public class Mime {
public init(args: Array<TypeMap>) // 构造函数,依次 define 每个 TypeMap
public func define(typeMap: TypeMap, force: Bool): Mime // 定义映射,force=true 强制覆盖
public func define(typeMap: TypeMap): Mime // 定义映射,默认不覆盖
public func getType(path: String): ?String // 文件路径或扩展名 -> MIME 类型
public func getExtension(typ: String): ?String // MIME 类型 -> 默认扩展名
public func getAllExtensions(typ: String): ?Set<String> // MIME 类型 -> 全部扩展名
}
TypeMap 的本质是 HashMap<String, Array<String>>:键为 MIME 类型,值为该类型关联的扩展名数组,数组第一个扩展名作为该类型的默认扩展名。
全局对象:MIME 与 MIME_LITE
在 src/mime.cj 末尾,两个全局对象通过构造函数 + 冻结机制生成:
public let MIME: Mime = Mime(standardTypes, otherTypes)._freeze()
public let MIME_LITE: Mime = Mime(standardTypes)._freeze()
MIME:全量版本,由standardTypes与otherTypes两套数据构建,覆盖 800+ 类型、1,000+ 扩展名;MIME_LITE:轻量级替代版本,仅使用standardTypes,移除了非官方类型(即"prs."、"x-"与"vnd.*"前缀的 MIME 类型)。
两者都经过 _freeze() 冻结,任何对内置对象调用 define() 的行为都会抛出 IllegalStateException('define() not allowed for built-in Mime objects.')(见 src/mime.cj 的 check() 私有方法)。
完整版与精简版的最小用法:
import mime.*
MIME.getType('txt') // ⇨ 'text/plain'
MIME.getExtension('text/plain') // ⇨ 'txt'
import MIME.*
MIME_LITE.getType('txt') // ⇨ 'text/plain'
MIME_LITE.getExtension('text/plain') // ⇨ 'txt'
注意:两段示例的导入写法在 README 中分别为
import mime.*与import MIME.*,其中精简版示例中的包名大小写为原文档写法,实际使用以你的cjpm.toml包名mime为准。
MIME.getType(pathOrExtension):扩展名 / 路径 → MIME 类型
getType 接受文件路径或扩展名,返回匹配的 MIME 类型;若扩展名未检测到或未经验证,返回 None。
MIME.getType('js') // ⇨ 'text/javascript'
MIME.getType('json') // ⇨ 'application/json'
MIME.getType('txt') // ⇨ 'text/plain'
MIME.getType('dir/text.txt') // ⇨ 'text/plain'
MIME.getType('dir\\text.txt') // ⇨ 'text/plain'
MIME.getType('.text.txt') // ⇨ 'text/plain'
MIME.getType('.txt') // ⇨ 'text/plain'
未命中时返回 None:
MIME.getType('foo/txt') // ⇨ None
MIME.getType('bogus_type') // ⇨ None
边界行为:从源码看解析规则
getType 的实现(src/mime.cj)包含一套完整的分支处理逻辑:
- 空字符串直接返回
None; - 路径剥离:从后向前查找最后一个
/或\,仅取文件名部分(因此dir/text.txt与dir\\text.txt均可解析),随后统一toAsciiLower()转为小写; - 隐藏文件:以
.开头且无第二个点(如.txt且无路径前缀)时,将点后部分视作扩展名查询;纯.或带路径的隐藏文件(如/path/to/.json)返回None; - 多级扩展名:如
.config.json这类"第二个点"存在的输入,取最后一个点之后的子串作为扩展名,因此.config.json→application/json; - 无扩展名:文件名中没有
.时,若带路径则视为无扩展名文件返回None(如/path/to/json),若为裸字符串则整个作为扩展名查询(如txt); - 尾点:最后一个点位于末尾(如
text.)视为无效扩展名,返回None。
底层查询是一张 extensionToType: HashMap<String, String>,即"扩展名 → 类型"反向索引,复杂度为 O(1)。
测试用例佐证
test/HLT/test_Mime_01.cj 覆盖了自定义对象的基础查询与路径解析:
var typeMap_01: TypeMap = HashMap<String, Array<String>>()
typeMap_01.add("text/plain", ["h", "txt", "bas", "c"])
var arrs: Array<TypeMap> = [typeMap_01]
var mime_02 = Mime(arrs)
@Assert(mime_02.getType("h"), "text/plain")
@Assert(mime_02.getType("./ext.txt"), "text/plain")
@Assert(mime_02.getType("../../ext.cj").isNone(), true)
test/LLT/mime_test.cj 进一步验证了大小写不敏感(TEXT.TXT → text/plain)、中文文件名(中文.txt → text/plain)、Windows 路径(c:\path\to\page.html → text/html)以及多种 None 场景(/txt、\txt、text.nope、/path/to/.json、仓颉 等)。
MIME.getExtension(type):MIME 类型 → 默认扩展名
getExtension 根据 MIME 类型返回其默认(第一个)扩展名,并自动忽略字符集参数——这在处理 HTTP Content-Type 头时非常实用:
MIME.getExtension('text/plain') // ⇨ 'txt'
MIME.getExtension('application/json') // ⇨ 'json'
MIME.getExtension('text/html; charset=utf8') // ⇨ 'html'
源码实现(src/mime.cj)关键步骤:
- 以
;分割并取第一段,剥离charset等参数; trimAscii()去除首尾空白;toAsciiLower()统一小写;- 在
typeToExtension: HashMap<String, String>(类型 → 默认扩展名)中查询。
因此 ' text/html; charset=UTF-8'、'text/html;charset=UTF-8'、'text/HTML ; charset=UTF-8' 等变体均能正确返回 html,这一点在 test/LLT/mime_test.cj 的 charset_parameter 用例中有十余组断言验证。
MIME.getAllExtensions(type):获取全部扩展名
当一个 MIME 类型对应多个扩展名时(如 image/jpeg 同时映射 jpeg、jpg、jpe),可用 getAllExtensions 一次性取出:
MIME.getAllExtensions('image/jpeg') // ⇨ [ 'jpeg', 'jpg', 'jpe' ]
返回值类型为 ?Set<String>,底层对应 typeToExtensions: HashMap<String, Set<String>>(类型 → 扩展名集合)。注意该集合包含带 * 前缀标记的扩展名(详见下文 define 机制),test/HLT/test_Mime_06.cj 对此有直接验证:
typeMap_01.add("image/gif", ["*gif", "p*ng", "jpeg*"])
mime_01.define(typeMap_01)
// getAllExtensions("image/gif") 包含 "gif"、"p*ng"、"jpeg*"
自定义 Mime 对象与 define() 映射扩展
默认的 MIME / MIME_LITE 不可更改。若需要自定义类型映射,可创建自己的 Mime 对象:
let customTypes : HashMap<String,Array<String>> = HashMap()
let mime = Mime(customTypes)
构造函数接收 Array<TypeMap>,内部会对每个 TypeMap 依次调用 define()。因此,Mime(standardTypes, otherTypes) 等价于 Mime().define(standardTypes).define(otherTypes)。
定义 type→extensions 映射
define() 用于追加类型到扩展名的映射:
...
mime.define(HashMap([('text/x-abc', ['abc', 'abcd'])]))
mime.getType('abcd') // ⇨ 'text/x-abc'
mime.getExtension('text/x-abc') // ⇨ 'abc'
当尝试把某扩展名映射到一个已存在的类型时,默认会抛出错误,除非传入 force=true:
public func define(typeMap: TypeMap, force: Bool): Mime
public func define(typeMap: TypeMap): Mime // 等价于 define(typeMap, false)
define 的源码语义
define(src/mime.cj)是理解整个库的关键,其处理流程如下:
- 大小写规范化:类型
typ.toAsciiLower(),每个扩展名ext.toAsciiLower(),保证查询大小写不敏感; *前缀处理:以*开头的扩展名(如'*js'、'*mp4')会被剥离*后加入该类型的扩展名集合,也可以成为默认扩展名,但不会建立"扩展名 → 类型"的反向映射(starred时continue跳过extensionToType.add)。这正是 src/standard.cj 中('application/javascript', ['*js'])与('text/javascript', ['js', 'mjs'])同时存在、而getType('js')返回text/javascript的原因;- 默认扩展名:每个类型扩展名数组的第一个(含
*前缀剥离后的)元素被记入typeToExtension; - 冲突检测:若某扩展名已被映射到其他类型,且
force为false,抛出:
该错误文案与 test/HLT/test_Mime_02.cj 中的断言完全一致;IllegalStateException: "text/pl -> h" conflicts with "Some(text/plain) -> h". Pass `force=true` to override this definition. force=true时覆盖extensionToType中的旧映射,test/HLT/test_Mime_03.cj 验证了define(typeMap_02, true)后getType('h')返回新类型text/pl。
源码内部结构:三张索引表
Mime 类内部维护三张私有哈希表(src/mime.cj):
| 字段 | 类型 | 用途 |
|---|---|---|
extensionToType |
HashMap<String, String> |
扩展名 → 类型(getType 查询) |
typeToExtension |
HashMap<String, String> |
类型 → 默认扩展名(getExtension 查询) |
typeToExtensions |
HashMap<String, Set<String>> |
类型 → 全部扩展名集合(getAllExtensions 查询) |
数据来源上,src/standard.cj 定义了约 190 组标准类型(application/*、audio/*、font/*、image/*、message/*、model/*、text/*、video/*),src/other.cj 则补充了 application/prs.*、application/vnd.*、application/x-*、image/vnd.*、text/vnd.*、video/vnd.* 等非官方类型(含 apk、docx、xlsx、pptx、rar、7z、torrent 等高频扩展名)。* 前缀扩展名的使用贯穿两份数据,用于解决"同一扩展名被多个类型声明"时的默认归属问题。
测试与质量保障
项目测试覆盖完整,HLT 目录下共 7 个测试类:
- test_Mime_01.cj:构造函数 +
getType路径解析与None分支; - test_Mime_02.cj:
define()默认行为、重复定义、冲突异常; - test_Mime_03.cj:
define(force=true)强制覆盖; - test_Mime_04.cj:
define(force=false)与默认行为等价; - test_Mime_05.cj:
getExtension默认扩展名与*扩展名不作为默认值; - test_Mime_06.cj:
getAllExtensions集合完整性; - test_Mime_07.cj:基于 mime_common.cj 与 contentType.cj 的大规模数据驱动测试(含
*扩展名剥离逻辑)。
LLT 目录下另有:
- mime_test.cj:针对全局
MIME对象的全面行为测试(大小写、路径、charset、中文、构造器、define、*扩展名); - vendor_test.cj:与 MDN 常见类型表、
mime-types扩展名库逐条比对,输出并断言已知差异(例如exe在库中归属application/octet-stream,而部分第三方表记作application/x-msdos-program); - bench.cj:
@Bench基准用例(MIME.getExtension('image/jpeg'))与cold()冷启动用例。
README 徽标声明项目构建通过(build-pass),cjcov 覆盖率 99.6%,即核心查询与映射逻辑均已纳入测试矩阵。
约束与限制
项目在以下工具链版本验证通过:
| 编号 | 依赖构建工具 | 版本号 |
|---|---|---|
| 1 | cjc | v1.1.3 |
该版本号同时写入 cjpm.toml 的 cjc-version 字段,使用时请确保仓颉编译器版本匹配。
开源协议与参与贡献
本项目基于 MIT License 开源,可自由使用与参与。欢迎提交 PR、Issue 或任何形式的贡献,贡献者信息见 README.md 末尾。
小结
mime4cj 以"扩展名 ↔ MIME 类型"双向索引为核心,通过 getType / getExtension / getAllExtensions 三个查询 API 覆盖了 Web 开发中最常见的 Content-Type 判定需求,Mime 类与 define(force) 机制则保证了映射的可扩展性与冲突可控性,MIME / MIME_LITE 双对象设计兼顾了完整性与体积诉求。若你需要为仓颉项目接入 MIME 类型识别能力,从 src/mime.cj 与 doc/feature_api.md 入手即可快速掌握全部能力。