feat(documents): PDF 文本层抽取(S6 第一段;扫描件仍诚实拒绝)
- 依赖:github.com/ledongthuc/pdf(**BSD-3-Clause**,非 MIT —— 子代理核了 LICENSE 并纠正了任务书的写法;零传递依赖;无 cgo/无外部进程;体积 strip 后 +285.5 KiB / +0.74%,在 detached worktree 里量取,产出写到仓库外)。被否候选:pdfcpu 无文本抽取、rsc.io/pdf 停更、pdfium/poppler/mupdf 需 cgo 撞红线。
- 实现:魔数 %PDF- 是唯一入口判定(名字/声明指向 PDF 而字节无魔数 ⇒ ErrUnsupported,绝不退回纯文本抽);逐页 GetPlainText(页间仅去首尾空白);ExtractorVersion v2→v3(所有 sidecar 一次性重抽);页数上限 5000 + 字节安全上限复用;新增哨兵 ErrPDFEncrypted / ErrPDFDamaged。
- 诚实边界:无文本层(扫描件)⇒ ErrNoTextLayer 且文案含「图片型/无文本层 PDF,需要 OCR(当前未做)」;加密/损坏各自可判、绝不 panic、不回空正文假装成功。
- 白名单与成对裁决:.pdf 纳入(22→23),且 ContentAllowed 做双向裁决(.pdf 只收嗅探为 application/pdf;PDF 字节只收在 .pdf 名下);读侧真 PDF 如实下发 application/pdf,仍 attachment + nosniff(不可内联)。
- 1 处 panic 兜底已如实登记:该库对「xref 指向文件外」畸形件会在 NumPage() 直接 panic(已用仓库外程序复现),单二进制常驻后端不能因 587 字节上传件崩掉 ⇒ defer recover 折成 ErrPDFDamaged;损坏类主用例仍走库的真出错路径。
- 主会话独立验收:隔离 worktree(B1 的 14 文件)build/vet 绿、全树 59 包 0 FAIL、PDF 用例 13 PASS、双平台交叉编译绿;变异独立复现(魔数裁决短路 ⇒ 魔数/成对裁决用例红)。
- ⚠️ 契约漂移(已排 C2):Docs 仍写「22 项白名单 / 嗅探仅放行 text/plain」,与本切片的 23 项 + 成对裁决不一致 ⇒ 需升 v0.61 登记 .pdf 与新的边界。
This commit is contained in:
1 parent
c4e09afd38
commit
f82c76df27
14 files changed
+926
-94
No files matched your search
@@ -8,8 +8,9 @@ import (
|
||||
)
|
||||
|
||||
// 这些测试钉住抽取器的对外事实(design/24 §4.1/§4.3):纯文本无损、CSV/TSV 带
|
||||
// 行数/列名摘要、超预算**诚实截断**、非法 UTF-8 不 panic 也不报废、PDF/Office
|
||||
// 与二进制**诚实拒绝**(哨兵错误,用 errors.Is 判,绝不按错误文本匹配)。
|
||||
// 行数/列名摘要、超预算**诚实截断**、非法 UTF-8 不 panic 也不报废、Office 与
|
||||
// 二进制**诚实拒绝**(哨兵错误,用 errors.Is 判,绝不按错误文本匹配)。PDF 的
|
||||
// 文本层抽取另有 extract_pdf_test.go。
|
||||
|
||||
// csvName / tsvName 是合法的 S1 存储名形态(32 位十六进制 + 白名单扩展名),
|
||||
// 使抽取器之外的行为(sidecar 键、路径校验)与真实上传同形。
|
||||
@@ -17,6 +18,7 @@ const (
|
||||
csvName = "0123456789abcdef0123456789abcdef.csv"
|
||||
tsvName = "0123456789abcdef0123456789abcdef.tsv"
|
||||
txtName = "0123456789abcdef0123456789abcdef.txt"
|
||||
pdfName = "0123456789abcdef0123456789abcdef.pdf"
|
||||
)
|
||||
|
||||
// TestExtract_PlainTextIsLossless 验证纯文本抽取逐字无损、统计与版本随结果返回。
|
||||
@@ -184,20 +186,29 @@ func TestExtract_RefusesUnsupportedContent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtract_RefusesNoTextLayer 钉住 PDF/Office 的**诚实拒绝**(design/24
|
||||
// §8-1 的默认裁决):扩展名或嗅探所得 MIME 任一指向该类格式即 ErrNoTextLayer。
|
||||
// TestExtract_RefusesNoTextLayer 钉住 Office 与「声明像是 PDF 却不是」的诚实拒绝:
|
||||
// Office 扩展名或嗅探所得 MIME 指向该类格式即 ErrNoTextLayer;而**名字/声明指向
|
||||
// PDF、字节却不是真 PDF** 时是「说的和做的不一致」,按不支持拒绝 —— 两条都不允许
|
||||
// 退回按纯文本抽(PDF 文本层本身能抽,用例见 extract_pdf_test.go)。
|
||||
func TestExtract_RefusesNoTextLayer(t *testing.T) {
|
||||
t.Parallel()
|
||||
pdf := []byte("%PDF-1.7\n1 0 obj\n")
|
||||
if _, err := Extract("report.pdf", "text/plain", pdf); !errors.Is(err, ErrNoTextLayer) {
|
||||
t.Fatalf("Extract(.pdf) = %v, want ErrNoTextLayer", err)
|
||||
}
|
||||
if _, err := Extract("report.docx", "text/plain", []byte("docx")); !errors.Is(err, ErrNoTextLayer) {
|
||||
t.Fatalf("Extract(.docx) = %v, want ErrNoTextLayer", err)
|
||||
}
|
||||
// 扩展名看着像文本、嗅探结果却是 PDF:MIME 只作否决参考,照样拒绝。
|
||||
if _, err := Extract(txtName, "application/pdf", []byte("hi")); !errors.Is(err, ErrNoTextLayer) {
|
||||
t.Fatalf("Extract(pdf mime) = %v, want ErrNoTextLayer", err)
|
||||
if _, err := Extract("sheet.xlsx", "text/plain", []byte("xlsx")); !errors.Is(err, ErrNoTextLayer) {
|
||||
t.Fatalf("Extract(.xlsx) = %v, want ErrNoTextLayer", err)
|
||||
}
|
||||
if _, err := Extract(txtName, "application/msword", []byte("doc")); !errors.Is(err, ErrUnsupported) {
|
||||
t.Fatalf("Extract(word mime) = %v, want ErrUnsupported", err)
|
||||
}
|
||||
// 扩展名看着像 PDF、内容却是纯文本:绝不能按文本抽(那种内容会以「正文」
|
||||
// 身份进模型上下文),也不能报成「无文本层」。
|
||||
if _, err := Extract(pdfName, "text/plain", []byte("hi")); !errors.Is(err, ErrUnsupported) {
|
||||
t.Fatalf("Extract(.pdf as text) = %v, want ErrUnsupported", err)
|
||||
}
|
||||
// 扩展名看着像文本、嗅探结果却是 PDF、字节又不是真 PDF:同样拒绝。
|
||||
if _, err := Extract(txtName, PDFMIME, []byte("hi")); !errors.Is(err, ErrUnsupported) {
|
||||
t.Fatalf("Extract(pdf mime) = %v, want ErrUnsupported", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user