// 检测引擎。
//
// 整体流程:
// 1. 用户规则(Registry);
// 2. 按签名表顺序做魔数匹配(强签名在前,弱签名在后);
// 3. 命中容器类签名后交给结构解析层判定(ZIP 中央目录 / OLE2 目录流 /
// ISO-BMFF box / RIFF chunk / EBML DocType / PE 头);
// 4. 命中弱签名时做结构校验,校验失败则回落;
// 5. 用户检测器;
// 6. 保守的文本启发式;
// 7. 仍然无法判断则返回 unknown。
//
// 置信度由证据累加得出(见 `confidence_of_score`),不是人工标注的标签。
// 所有字节访问都先做边界检查,因此任意输入都不会触发越界。
// ---------------------------------------------------------------------------
// 证据与结果构造
// ---------------------------------------------------------------------------
///|
const W_STRONG : Int = 90
const W_WEAK : Int = 20
const W_STRUCTURE : Int = 40
const W_DECISIVE : Int = 70
const W_HEURISTIC : Int = 30
///|
fn evidence(
kind : String,
label : String,
offset : Int,
matched_len : Int,
weight : Int,
) -> Evidence {
{ kind, label, offset, matched_len, weight }
}
///|
fn make_detection(
file_type : FileType?,
reason : String,
evidence : Array[Evidence],
candidates : Array[FileType],
) -> Detection {
let mut score = 0
for item in evidence {
score = score + item.weight
}
if score > 100 {
score = 100
}
let offset = if evidence.length() > 0 { evidence[0].offset } else { 0 }
let matched_len = if evidence.length() > 0 {
evidence[0].matched_len
} else {
0
}
let confidence = match file_type {
Some(_) => confidence_of_score(score)
None => Unknown
}
{
file_type,
confidence,
reason,
offset,
matched_len,
candidates,
score,
evidence,
}
}
///|
fn unknown_detection(reason : String) -> Detection {
make_detection(None, reason, [], [])
}
///|
fn signature_evidence(sig : Signature) -> Evidence {
evidence(
"magic",
sig.label,
sig.offset,
sig.magic.length(),
if sig.strong {
W_STRONG
} else {
W_WEAK
},
)
}
///|
fn certain_detection(sig : Signature) -> Detection {
make_detection(
Some(sig.file_type),
sig.label,
[signature_evidence(sig)],
[],
)
}
///|
/// 由「魔数 + 结构证据」构造结果。
fn structured_detection(
sig : Signature,
file_type : FileType,
reason : String,
structure_label : String,
structure_weight : Int,
) -> Detection {
make_detection(
Some(file_type),
reason,
[
signature_evidence(sig),
evidence("structure", structure_label, 0, 0, structure_weight),
],
[],
)
}
///|
fn with_candidates(
sig : Signature,
file_type : FileType,
reason : String,
structure_label : String,
candidates : Array[FileType],
) -> Detection {
make_detection(
Some(file_type),
reason,
[
signature_evidence(sig),
evidence("structure", structure_label, 0, 0, W_STRUCTURE),
],
candidates,
)
}
// ---------------------------------------------------------------------------
// 容器消歧:全部基于结构解析
// ---------------------------------------------------------------------------
///|
/// 读取 ZIP 第一个条目的 mimetype 内容(EPUB / ODF 规范要求它 stored 且排第一)。
fn zip_declared_mimetype(data : Bytes, archive : ZipArchive) -> String? {
if archive.entries.length() == 0 {
return None
}
let first = archive.entries[0]
if first.name != "mimetype" || first.compression != 0 {
return None
}
match zip_entry_content(data, first) {
Some(content) => Some(raw_string(content, 0, content.length()))
None => None
}
}
///|
fn refine_zip(sig : Signature, data : Bytes) -> Detection {
let archive = match parse_zip(data) {
Some(a) => a
None =>
return make_detection(
Some(zip),
"ZIP 文件头,但中央目录无法解析(可能被截断或不是 ZIP)",
[signature_evidence(sig)],
[],
)
}
let names = zip_entry_names(archive)
let parsed = "\{archive.entries.length()}/\{archive.declared_entries}"
let layout = "中央目录解析成功(\{parsed} 条目)"
match zip_declared_mimetype(data, archive) {
Some(mime) =>
if mime == "application/epub+zip" {
return structured_detection(
sig, epub, "ZIP 首条目 mimetype 声明为 EPUB", "首条目是 stored 的 mimetype", W_DECISIVE,
)
} else if mime == "application/vnd.oasis.opendocument.text" {
return structured_detection(
sig, odt, "ZIP 首条目 mimetype 声明为 OpenDocument text", "首条目是 stored 的 mimetype", W_DECISIVE,
)
} else if mime == "application/vnd.oasis.opendocument.spreadsheet" {
return structured_detection(
sig, ods, "ZIP 首条目 mimetype 声明为 OpenDocument spreadsheet", "首条目是 stored 的 mimetype", W_DECISIVE,
)
} else if mime == "application/vnd.oasis.opendocument.presentation" {
return structured_detection(
sig, odp, "ZIP 首条目 mimetype 声明为 OpenDocument presentation", "首条目是 stored 的 mimetype", W_DECISIVE,
)
}
None => ()
}
// OOXML:必须有 [Content_Types].xml,且主文档部件独立出现
if contains_name(names, "[Content_Types].xml") {
if contains_name(names, "word/document.xml") {
return structured_detection(
sig, docx, "OOXML:存在 word/document.xml 与 [Content_Types].xml", layout, W_STRUCTURE,
)
}
if contains_name(names, "xl/workbook.xml") {
return structured_detection(
sig, xlsx, "OOXML:存在 xl/workbook.xml 与 [Content_Types].xml", layout, W_STRUCTURE,
)
}
if contains_name(names, "ppt/presentation.xml") {
return structured_detection(
sig, pptx, "OOXML:存在 ppt/presentation.xml 与 [Content_Types].xml", layout, W_STRUCTURE,
)
}
}
if contains_name(names, "FixedDocSeq.fdseq") {
return structured_detection(
sig, xps, "XPS:存在 FixedDocSeq.fdseq", layout, W_STRUCTURE,
)
}
if contains_name(names, "AndroidManifest.xml") &&
contains_name(names, "classes.dex") {
return structured_detection(
sig, apk, "APK:同时存在 AndroidManifest.xml 与 classes.dex", layout, W_DECISIVE,
)
}
if contains_name(names, "META-INF/MANIFEST.MF") {
return structured_detection(
sig, jar, "JAR:存在 META-INF/MANIFEST.MF 且无 classes.dex", layout, W_STRUCTURE,
)
}
structured_detection(sig, zip, "ZIP 中央目录结构有效,未发现更具体的容器特征", layout, W_STRUCTURE)
}
///|
fn refine_ole2(sig : Signature, data : Bytes) -> Detection {
match parse_ole2_streams(data) {
Some(names) =>
if contains_name(names, "WordDocument") {
return structured_detection(
sig, doc, "OLE2 目录中存在 WordDocument 流", "FAT 与目录扇区解析成功", W_DECISIVE,
)
} else if contains_name(names, "Workbook") ||
contains_name(names, "Book") {
return structured_detection(
sig, xls, "OLE2 目录中存在 Workbook/Book 流", "FAT 与目录扇区解析成功", W_DECISIVE,
)
} else if contains_name(names, "PowerPoint Document") {
return structured_detection(
sig, ppt, "OLE2 目录中存在 PowerPoint Document 流", "FAT 与目录扇区解析成功", W_DECISIVE,
)
}
None => ()
}
with_candidates(
sig,
ole2,
"OLE2 复合文档头;目录结构无法解析或未包含已知流",
"仅头部校验通过",
[doc, xls, ppt],
)
}
///|
fn refine_riff(sig : Signature, data : Bytes) -> Detection {
let info = match parse_riff(data) {
Some(v) => v
None =>
return with_candidates(
sig, riff, "RIFF 容器,但 chunk 结构无法解析", "header 校验通过", [wav, avi, webp],
)
}
if info.form == "WAVE" &&
contains_name(info.chunks, "fmt ") &&
contains_name(info.chunks, "data") {
return structured_detection(
sig, wav, "RIFF/WAVE,且 fmt 与 data chunk 都存在", "chunk 遍历命中 fmt + data", W_DECISIVE,
)
}
if info.form == "AVI " &&
(contains_name(info.chunks, "hdrl") || contains_name(info.chunks, "movi")) {
return structured_detection(
sig, avi, "RIFF/AVI,且 hdrl 或 movi chunk 存在", "chunk 遍历命中 AVI 必需块", W_DECISIVE,
)
}
if info.form == "WEBP" &&
(contains_name(info.chunks, "VP8 ") ||
contains_name(info.chunks, "VP8L") ||
contains_name(info.chunks, "VP8X")) {
return structured_detection(
sig, webp, "RIFF/WEBP,且存在 VP8/VP8L/VP8X chunk", "chunk 遍历命中 WebP 编码块", W_DECISIVE,
)
}
with_candidates(
sig,
riff,
"RIFF 容器,子类型未知或缺少必需 chunk",
"chunk 遍历未命中已知子类型",
[wav, avi, webp],
)
}
///|
fn refine_iff(sig : Signature, data : Bytes) -> Detection {
match parse_iff(data) {
Some(info) =>
if (info.form == "AIFF" || info.form == "AIFC") &&
contains_name(info.chunks, "COMM") {
return structured_detection(
sig, aiff, "IFF/AIFF,且存在 COMM chunk", "chunk 遍历命中 COMM", W_DECISIVE,
)
}
None => ()
}
with_candidates(
sig, iff, "IFF 容器,但子类型未知", "header 校验通过", [aiff],
)
}
///|
fn refine_ftyp(sig : Signature, data : Bytes) -> Detection {
let brands = bmff_brands(data)
let handlers = bmff_handlers(data)
let brand_label = if brands.length() > 0 {
"ftyp major brand = \{brands[0]}"
} else {
"ftyp 已解析"
}
if contains_name(brands, "avif") || contains_name(brands, "avis") {
return structured_detection(
sig, avif, "ISO-BMFF:brand 为 avif/avis", brand_label, W_DECISIVE,
)
}
if contains_name(brands, "heic") ||
contains_name(brands, "heix") ||
contains_name(brands, "hevc") ||
contains_name(brands, "hevx") ||
contains_name(brands, "mif1") ||
contains_name(brands, "msf1") {
return structured_detection(
sig, heic, "ISO-BMFF:brand 属于 HEIF 系列", brand_label, W_DECISIVE,
)
}
if contains_name(brands, "M4A ") || contains_name(brands, "M4B ") {
return structured_detection(
sig, m4a, "ISO-BMFF:brand 为 M4A/M4B", brand_label, W_DECISIVE,
)
}
if contains_name(brands, "3gp4") ||
contains_name(brands, "3gp5") ||
contains_name(brands, "3gp6") ||
contains_name(brands, "3g2a") {
return structured_detection(
sig, threegp, "ISO-BMFF:brand 属于 3GP 系列", brand_label, W_DECISIVE,
)
}
if contains_name(brands, "qt ") {
return structured_detection(
sig, mov, "ISO-BMFF:brand 为 QuickTime", brand_label, W_DECISIVE,
)
}
if contains_name(handlers, "vide") {
return structured_detection(
sig, mp4, "ISO-BMFF:mdia/hdlr 声明 vide", "box 树下钻到 hdlr = vide", W_DECISIVE,
)
}
if contains_name(handlers, "soun") {
return structured_detection(
sig, m4a, "ISO-BMFF:mdia/hdlr 仅声明 soun", "box 树下钻到 hdlr = soun", W_DECISIVE,
)
}
if contains_name(brands, "isom") ||
contains_name(brands, "iso2") ||
contains_name(brands, "mp41") ||
contains_name(brands, "mp42") ||
contains_name(brands, "M4V ") ||
contains_name(brands, "dash") {
return with_candidates(
sig,
mp4,
"ISO-BMFF:brand 属于 MP4 系列,但未找到 handler",
brand_label,
[mov, m4a],
)
}
with_candidates(
sig,
ftyp,
"ISO-BMFF 容器,但 brand 未知",
"box 头解析通过",
[mp4, mov, heic],
)
}
///|
fn refine_ebml(sig : Signature, data : Bytes) -> Detection {
match ebml_doctype(data) {
Some(doc_type) =>
if doc_type == "webm" {
return structured_detection(
sig, webm, "EBML DocType = webm", "vint 解析出 DocType 元素", W_DECISIVE,
)
} else if doc_type == "matroska" {
return structured_detection(
sig, mkv, "EBML DocType = matroska", "vint 解析出 DocType 元素", W_DECISIVE,
)
}
None => ()
}
with_candidates(
sig, ebml, "EBML 容器,但未解析出 DocType", "EBML 头结构有效", [mkv, webm],
)
}
///|
fn refine_ogg(sig : Signature, data : Bytes) -> Detection {
if data.length() >= 27 {
let segments = data[26].to_int()
if segments > 0 && 27 + segments < data.length() {
let packet_start = 27 + segments
let packet_len = data[27].to_int()
match slice_bytes(data, packet_start, packet_start + packet_len) {
Some(packet) =>
if match_at(packet, 0, b"OpusHead") {
return structured_detection(
sig, opus, "Ogg 首页首包为 OpusHead", "按 Ogg 页布局解析出首包", W_DECISIVE,
)
}
None => ()
}
}
}
certain_detection(sig)
}
///|
fn refine_cafebabe(sig : Signature, data : Bytes) -> Detection {
match u32_be(data, 4) {
Some(count) =>
if count >= 1 && count <= 16 {
return structured_detection(
sig, macho, "Mach-O universal binary:nfat_arch 合理", "fat header 的 nfat_arch 在 1..16", W_STRUCTURE,
)
}
None => ()
}
match u16_be(data, 6) {
Some(major) =>
if major >= 45 && major <= 70 {
return structured_detection(
sig, java_class, "Java class:主版本号在 45..70", "class 文件版本字段校验通过", W_STRUCTURE,
)
}
None => ()
}
with_candidates(
sig,
cafebabe,
"CAFEBABE 头,但无法确定是 Mach-O fat 还是 Java class",
"两种结构的判别字段都不成立",
[macho, java_class],
)
}
///|
fn refine_ttf_raw(sig : Signature, data : Bytes) -> Detection {
match u16_be(data, 4) {
Some(tables) =>
if tables >= 1 && tables <= 512 {
structured_detection(
sig, ttf, "TrueType sfnt:表数量合理", "sfnt offset table 的 numTables 在 1..512", W_STRUCTURE,
)
} else {
unknown_detection("以 0x00010000 开头,但 sfnt 表数量异常")
}
None => unknown_detection("输入过短,无法校验 TrueType 表数量")
}
}
///|
fn refine_pe(sig : Signature, data : Bytes) -> Detection {
match parse_pe(data) {
Some(info) => {
let kind = if info.is_dll { "DLL" } else { "EXE" }
return structured_detection(
sig,
pe,
"PE 结构有效:\{pe_machine_name(info)},\{kind},\{info.sections} 个节",
"e_lfanew 指向合法 PE 头,COFF/Optional Header 校验通过",
W_DECISIVE,
)
}
None => ()
}
let limit = if data.length() < 4096 { data.length() } else { 4096 }
if looks_like_text(data, limit) {
return detect_text(data)
}
make_detection(
Some(pe),
"MZ 头,但 e_lfanew 指向的不是合法 PE 头",
[signature_evidence(sig)],
[],
)
}
///|
/// BMP 的结构校验:保留字段必须为 0,DIB header 大小必须在已知取值集合内。
fn refine_bmp(sig : Signature, data : Bytes) -> Detection {
if data.length() >= 30 {
let reserved = u32_le(data, 6)
let dib = u32_le(data, 14)
match reserved {
Some(r) =>
match dib {
Some(size) =>
if r == 0 &&
(size == 12 ||
size == 40 ||
size == 52 ||
size == 56 ||
size == 64 ||
size == 108 ||
size == 124) {
return structured_detection(
sig,
bmp,
"BMP 结构有效:DIB header = \{size}",
"保留字段为 0 且 DIB header 大小合法",
W_DECISIVE,
)
}
None => ()
}
None => ()
}
}
let limit = if data.length() < 4096 { data.length() } else { 4096 }
if looks_like_text(data, limit) {
return detect_text(data)
}
make_detection(
Some(bmp),
"BM 头,但 DIB header 未通过校验",
[signature_evidence(sig)],
[],
)
}
///|
/// PNM 的弱签名要求魔数后紧跟空白字符。
fn refine_pnm(sig : Signature, data : Bytes) -> Detection {
if data.length() > sig.magic.length() {
let next = data[sig.magic.length()].to_int()
if next == 32 || next == 9 || next == 10 || next == 13 {
return make_detection(
Some(pnm),
"PNM 魔数后紧跟空白字符",
[
signature_evidence(sig),
evidence("structure", "PNM 魔数后是分隔符", sig.magic.length(), 1, W_STRUCTURE),
],
[],
)
}
}
detect_text(data)
}
///|
fn refine_tar(sig : Signature, data : Bytes) -> Detection {
if tar_checksum_ok(data) {
return structured_detection(
sig,
tar,
"tar header 的八进制 checksum 校验通过",
"512 字节 header 的 checksum 与内容一致",
W_DECISIVE,
)
}
make_detection(
Some(tar),
"存在 ustar 标识,但 header checksum 不匹配",
[signature_evidence(sig)],
[],
)
}
///|
fn refine_postscript(sig : Signature, data : Bytes) -> Detection {
if match_at(data, 0, b"%!PS-AdobeFont") || match_at(data, 0, b"%!FontType1") {
return structured_detection(
sig, pfa, "PostScript Type 1 字体头", "Adobe Font 专用标识匹配", W_STRUCTURE,
)
}
certain_detection(sig)
}
///|
fn refine_asf(sig : Signature, _data : Bytes) -> Detection {
with_candidates(
sig,
asf,
"ASF 容器;仅凭头部无法区分 wma / wmv",
"ASF header GUID 匹配",
[wma, wmv],
)
}
///|
fn refine(sig : Signature, data : Bytes) -> Detection {
match sig.file_type.id {
"zip" => refine_zip(sig, data)
"riff" => refine_riff(sig, data)
"iff" => refine_iff(sig, data)
"ftyp" => refine_ftyp(sig, data)
"ebml" => refine_ebml(sig, data)
"ogg" => refine_ogg(sig, data)
"postscript" => refine_postscript(sig, data)
"asf" => refine_asf(sig, data)
"cafebabe" => refine_cafebabe(sig, data)
"ttf-raw" => refine_ttf_raw(sig, data)
"tar" => refine_tar(sig, data)
"pe" => refine_pe(sig, data)
"bmp" => refine_bmp(sig, data)
"pnm" => refine_pnm(sig, data)
"ole2" => refine_ole2(sig, data)
_ => certain_detection(sig)
}
}
// ---------------------------------------------------------------------------
// 文本启发式
// ---------------------------------------------------------------------------
///|
/// 判断前 `limit` 个字节是否「看起来像文本」。
///
/// 这里做保守的 UTF-8 结构检查:拒绝 NUL、明显不可打印的控制字符、
/// 非法首字节与错误的续字节。校验不完整时(样本在码点中间截断)按文本处理。
fn looks_like_text(data : Bytes, limit : Int) -> Bool {
let mut i = 0
while i < limit {
let b = data[i].to_int()
if b < 32 {
if b != 9 && b != 10 && b != 12 && b != 13 {
return false
}
i = i + 1
} else if b < 128 {
i = i + 1
} else {
let needed = if b >= 194 && b <= 223 {
1
} else if b >= 224 && b <= 239 {
2
} else if b >= 240 && b <= 244 {
3
} else {
return false
}
let mut j = 1
while j <= needed {
if i + j >= limit {
return true
}
let continuation = data[i + j].to_int()
if continuation < 128 || continuation > 191 {
return false
}
j = j + 1
}
i = i + needed + 1
}
}
true
}
///|
fn first_non_space(data : Bytes, limit : Int) -> Int {
let mut i = 0
while i < limit {
let b = data[i].to_int()
if b != 32 && b != 9 && b != 10 && b != 13 {
return i
}
i = i + 1
}
limit
}
///|
fn count_byte(data : Bytes, start : Int, end : Int, value : Int) -> Int {
let mut count = 0
let mut i = start
while i < end && i < data.length() {
if data[i].to_int() == value {
count = count + 1
}
i = i + 1
}
count
}
///|
/// 分隔符启发式:第一行至少一个分隔符,且第二行数量与第一行一致。
fn looks_like_delimited(data : Bytes, limit : Int, separator : Int) -> Bool {
let mut first_end = -1
let mut i = 0
while i < limit {
if data[i].to_int() == 10 {
first_end = i
break
}
i = i + 1
}
if first_end <= 0 {
return false
}
let first_count = count_byte(data, 0, first_end, separator)
if first_count == 0 {
return false
}
if first_end + 1 >= limit {
return true
}
let mut second_end = limit
let mut j = first_end + 1
while j < limit {
if data[j].to_int() == 10 {
second_end = j
break
}
j = j + 1
}
count_byte(data, first_end + 1, second_end, separator) == first_count
}
///|
fn starts_with_ci(data : Bytes, pattern : Bytes) -> Bool {
if data.length() < pattern.length() {
return false
}
let mut i = 0
while i < pattern.length() {
let mut a = data[i].to_int()
let mut b = pattern[i].to_int()
if a >= 65 && a <= 90 {
a = a + 32
}
if b >= 65 && b <= 90 {
b = b + 32
}
if a != b {
return false
}
i = i + 1
}
true
}
///|
fn text_detection(
file_type : FileType,
reason : String,
offset : Int,
matched_len : Int,
) -> Detection {
make_detection(
Some(file_type),
reason,
[evidence("heuristic", reason, offset, matched_len, W_HEURISTIC)],
[],
)
}
///|
fn detect_text(data : Bytes) -> Detection {
let limit = if data.length() < 4096 { data.length() } else { 4096 }
if !looks_like_text(data, limit) {
return unknown_detection("二进制内容,但没有命中已知签名")
}
let i = first_non_space(data, limit)
if i < limit {
let b = data[i].to_int()
if b == 123 || b == 91 {
return text_detection(json, "首个非空白字节是 { 或 [", i, 1)
}
}
if starts_with_ci(data, b" Bool {
if data.length() < 377 {
return false
}
let mut i = 0
let mut hits = 0
while i < data.length() && i < 188 * 8 {
if data[i].to_int() == 71 {
hits = hits + 1
}
i = i + 188
}
hits >= 3
}
///|
/// 内置签名与容器判定;没有命中时返回 `None`(不包含文本启发式)。
fn detect_signatures(data : Bytes) -> Detection? {
if looks_like_mpeg_ts(data) {
return Some(
make_detection(
Some(ts),
"MPEG-TS:0x47 同步字节按 188 字节周期出现",
[
evidence("structure", "0x47 同步字节周期为 188", 0, 1, W_DECISIVE),
],
[],
),
)
}
let mut i = 0
while i < signatures.length() {
let sig = signatures[i]
if match_at(data, sig.offset, sig.magic) {
return Some(refine(sig, data))
}
i = i + 1
}
None
}
///|
/// 根据字节内容识别文件类型(使用内置规则集)。
///
/// 该函数是纯函数:不读文件、不访问网络、不依赖系统状态。
pub fn detect(data : Bytes) -> Detection {
detect_with(Registry::new(), data)
}
///|
/// 使用自定义规则集与检测器识别文件类型。
///
/// 优先级:用户规则 → 内置签名与容器判定 → 用户检测器 → 文本启发式。
pub fn detect_with(registry : Registry, data : Bytes) -> Detection {
if data.length() == 0 {
return unknown_detection("空输入")
}
for rule in registry.rules {
match eval_matcher(rule.matcher, data) {
Some(offset) =>
return make_detection(
Some({
id: rule.id,
mime: rule.mime,
ext: rule.ext,
category: rule.category,
}),
rule.label,
[rule_evidence(rule, offset)],
[],
)
None => ()
}
}
match detect_signatures(data) {
Some(detection) => return detection
None => ()
}
for detector in registry.detectors {
match detector(data) {
Some(detection) => return detection
None => ()
}
}
detect_text(data)
}
///|
/// 内置签名条数。
pub fn signature_count() -> Int {
signatures.length()
}
///|
/// 由容器消歧推导出的类型:它们没有独立签名,但确实是可识别的已知类型。
fn container_types() -> Array[FileType] {
[
doc,
xls,
ppt,
docx,
xlsx,
pptx,
odt,
ods,
odp,
xps,
apk,
jar,
wav,
aiff,
opus,
m4a,
wma,
mp4,
mov,
mkv,
webm,
avi,
wmv,
threegp,
webp,
avif,
heic,
macho,
java_class,
]
}
///|
/// 全部已知类型(按首次出现顺序去重),用于 `--list-types`。
pub fn known_types() -> Array[FileType] {
let out : Array[FileType] = []
for sig in signatures {
if !contains_type(out, sig.file_type.id) {
out.push(sig.file_type)
}
}
for file_type in container_types() {
if !contains_type(out, file_type.id) {
out.push(file_type)
}
}
for file_type in text_types {
if !contains_type(out, file_type.id) {
out.push(file_type)
}
}
out
}
///|
fn contains_type(types : Array[FileType], id : String) -> Bool {
for file_type in types {
if file_type.id == id {
return true
}
}
false
}