Why: The registration gate classified Word packages from a hard-coded ZIP member path, allowing alternate main-part paths to bypass the gate and rejecting unrelated ZIP archives. Evidence: Focused OPC registration tests; go test -count=1 ./internal/tools; go test -count=1 ./...; go test -race -count=1 ./internal/tools ./internal/runmgr; go vet ./...; go build ./...; git diff --check. Impact: Resolve package-level officeDocument targets safely, classify all conventional Word main content types through OPC defaults or overrides, and fail closed on malformed, ambiguous, external, or traversing metadata while preserving non-Word artifacts. Refs: Task 10; /tmp/task10-backend-final-closure-last.md
553 lines
25 KiB
Go
553 lines
25 KiB
Go
package tools
|
||
|
||
import (
|
||
"archive/zip"
|
||
"bytes"
|
||
"encoding/json"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// zipOf 按序打包字符串部件(保持插入序便于断言)。
|
||
func zipOf(t *testing.T, files map[string]string) []byte {
|
||
t.Helper()
|
||
buf := new(bytes.Buffer)
|
||
w := zip.NewWriter(buf)
|
||
for name, body := range files {
|
||
f, err := w.Create(name)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := f.Write([]byte(body)); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
if err := w.Close(); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return buf.Bytes()
|
||
}
|
||
|
||
const (
|
||
gatePackageRelNS = "http://schemas.openxmlformats.org/package/2006/relationships"
|
||
gateOfficeRel = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
|
||
gateStrictOffice = "http://purl.oclc.org/ooxml/officeDocument/relationships/officeDocument"
|
||
gateContentNS = "http://schemas.openxmlformats.org/package/2006/content-types"
|
||
gateCTXML = `<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/></Types>`
|
||
gateSlide = `<?xml version="1.0"?><p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree/></p:cSld></p:sld>`
|
||
gateLayout = `<?xml version="1.0"?><p:sldLayout xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree/></p:cSld></p:sldLayout>`
|
||
gateMaster = `<?xml version="1.0"?><p:sldMaster xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:cSld><p:spTree/></p:cSld><p:sldLayoutIdLst><p:sldLayoutId id="2147483647" r:id="rId1"/></p:sldLayoutIdLst></p:sldMaster>`
|
||
gateTheme = `<?xml version="1.0"?><a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/>`
|
||
)
|
||
|
||
func relXML(rels ...string) string {
|
||
return `<?xml version="1.0"?><Relationships xmlns="` + gatePackageRelNS + `">` +
|
||
strings.Join(rels, "") + `</Relationships>`
|
||
}
|
||
|
||
func gateOfficePackage(t *testing.T, rels, contentTypes, mainPart string) []byte {
|
||
t.Helper()
|
||
files := map[string]string{
|
||
"_rels/.rels": rels,
|
||
"[Content_Types].xml": contentTypes,
|
||
}
|
||
if mainPart != "" {
|
||
files[strings.TrimPrefix(mainPart, "/")] = `<?xml version="1.0"?><root/>`
|
||
}
|
||
return zipOf(t, files)
|
||
}
|
||
|
||
func gateOfficeRelXML(relType, target, targetMode string) string {
|
||
mode := ""
|
||
if targetMode != "" {
|
||
mode = ` TargetMode="` + targetMode + `"`
|
||
}
|
||
return relXML(`<Relationship Id="rId1" Type="` + relType + `" Target="` + target + `"` + mode + `/>`)
|
||
}
|
||
|
||
func gateOverrideCT(part, contentType string) string {
|
||
return `<?xml version="1.0"?><Types xmlns="` + gateContentNS + `"><Override PartName="/` +
|
||
strings.TrimPrefix(part, "/") + `" ContentType="` + contentType + `"/></Types>`
|
||
}
|
||
|
||
const (
|
||
relSlide = `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>`
|
||
relMaster = `<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>`
|
||
relLayout = `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>`
|
||
relToMaster = `<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster1.xml"/>`
|
||
relTheme = `<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="../theme/theme1.xml"/>`
|
||
)
|
||
|
||
// gateCT 内容类型表:传入要 Override 的结构部件(缺了的用例自己构造残缺形态)。
|
||
func gateCT(parts ...string) string {
|
||
cts := map[string]string{
|
||
"ppt/presentation.xml": ctPresentation,
|
||
"ppt/slides/slide1.xml": ctSlide,
|
||
"ppt/slides/slide2.xml": ctSlide,
|
||
"ppt/slides/slide3.xml": ctSlide,
|
||
"ppt/slideLayouts/slideLayout1.xml": ctSlideLayout,
|
||
"ppt/slideMasters/slideMaster1.xml": ctSlideMaster,
|
||
"ppt/theme/theme1.xml": ctTheme,
|
||
}
|
||
out := `<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml"/><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>`
|
||
for _, p := range parts {
|
||
out += `<Override PartName="/` + p + `" ContentType="` + cts[p] + `"/>`
|
||
}
|
||
return out + `</Types>`
|
||
}
|
||
|
||
var gateAllParts = []string{
|
||
"ppt/presentation.xml", "ppt/slides/slide1.xml",
|
||
"ppt/slideLayouts/slideLayout1.xml", "ppt/slideMasters/slideMaster1.xml", "ppt/theme/theme1.xml",
|
||
}
|
||
|
||
// gateValidPptx 是结构合法的最小演示稿:r:id 引用、ID 清单与关系表一致、
|
||
// 内容类型全量覆盖——与 python-pptx 产物的关键形态一致。
|
||
func gateValidPptx(t *testing.T) []byte {
|
||
t.Helper()
|
||
return zipOf(t, map[string]string{
|
||
"[Content_Types].xml": gateCT(gateAllParts...),
|
||
"_rels/.rels": relXML(),
|
||
"ppt/presentation.xml": `<?xml version="1.0"?><p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId2"/></p:sldMasterIdLst><p:sldIdLst><p:sldId id="256" r:id="rId1"/></p:sldIdLst></p:presentation>`,
|
||
"ppt/_rels/presentation.xml.rels": relXML(relSlide, relMaster),
|
||
"ppt/slides/slide1.xml": gateSlide,
|
||
"ppt/slides/_rels/slide1.xml.rels": relXML(relLayout),
|
||
"ppt/slideLayouts/slideLayout1.xml": gateLayout,
|
||
"ppt/slideLayouts/_rels/slideLayout1.xml.rels": relXML(relToMaster),
|
||
"ppt/slideMasters/slideMaster1.xml": gateMaster,
|
||
"ppt/slideMasters/_rels/slideMaster1.xml.rels": relXML(relLayout, relTheme),
|
||
"ppt/theme/theme1.xml": gateTheme,
|
||
})
|
||
}
|
||
|
||
// gateIncident2Pptx 复刻第二起事故(部件齐全但引用断裂,上版门禁放行的形态):
|
||
// presentation/母版根元素漏 xmlns:r;sldIdLst 声明 3 页但关系表只有 rId3→slide1;
|
||
// [Content_Types] 缺 slide2/3 的 Override。三个 fix 开关逐项修复以隔离断言。
|
||
func gateIncident2Pptx(t *testing.T, fixNS, fixRels, fixCT bool) []byte {
|
||
t.Helper()
|
||
nsDecl := ` xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"`
|
||
pns, mns := "", ""
|
||
if fixNS {
|
||
pns, mns = nsDecl, nsDecl
|
||
}
|
||
pres := `<?xml version="1.0"?><p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"` + pns + `><p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId1"/></p:sldMasterIdLst><p:sldIdLst><p:sldId id="256" r:id="rId3"/><p:sldId id="257" r:id="rId4"/><p:sldId id="258" r:id="rId5"/></p:sldIdLst></p:presentation>`
|
||
master := `<?xml version="1.0"?><p:sldMaster xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"` + mns + `><p:cSld><p:spTree/></p:cSld><p:sldLayoutIdLst><p:sldLayoutId id="2147483647" r:id="rId1"/></p:sldLayoutIdLst></p:sldMaster>`
|
||
rels := `<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/></Relationships>`
|
||
if fixRels {
|
||
rels = relXML(
|
||
`<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/>`,
|
||
`<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/>`,
|
||
`<Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide2.xml"/>`,
|
||
`<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide3.xml"/>`)
|
||
}
|
||
ctParts := []string{"ppt/presentation.xml", "ppt/slideMasters/slideMaster1.xml",
|
||
"ppt/slideLayouts/slideLayout1.xml", "ppt/theme/theme1.xml", "ppt/slides/slide1.xml"}
|
||
if fixCT {
|
||
ctParts = append(ctParts, "ppt/slides/slide2.xml", "ppt/slides/slide3.xml")
|
||
}
|
||
return zipOf(t, map[string]string{
|
||
"[Content_Types].xml": gateCT(ctParts...),
|
||
"_rels/.rels": relXML(),
|
||
"ppt/presentation.xml": pres,
|
||
"ppt/_rels/presentation.xml.rels": rels,
|
||
"ppt/slides/slide1.xml": gateSlide,
|
||
"ppt/slides/slide2.xml": gateSlide,
|
||
"ppt/slides/slide3.xml": gateSlide,
|
||
"ppt/slides/_rels/slide1.xml.rels": relXML(relLayout),
|
||
"ppt/slides/_rels/slide2.xml.rels": relXML(relLayout),
|
||
"ppt/slides/_rels/slide3.xml.rels": relXML(relLayout),
|
||
"ppt/slideLayouts/slideLayout1.xml": gateLayout,
|
||
"ppt/slideLayouts/_rels/slideLayout1.xml.rels": relXML(relToMaster),
|
||
"ppt/slideMasters/slideMaster1.xml": master,
|
||
"ppt/slideMasters/_rels/slideMaster1.xml.rels": relXML(relLayout, relTheme),
|
||
"ppt/theme/theme1.xml": gateTheme,
|
||
})
|
||
}
|
||
|
||
func TestOoxmlGateValidPptx(t *testing.T) {
|
||
if err := ooxmlStructuralCheck("交付物.pptx", gateValidPptx(t)); err != nil {
|
||
t.Fatalf("valid pptx must pass: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestOoxmlGateIncident1PartsMissing(t *testing.T) {
|
||
// 第一类事故:部件整体缺失(无母版/主题/docProps、slide 无关系表)。
|
||
broken := zipOf(t, map[string]string{
|
||
"[Content_Types].xml": gateCTXML,
|
||
"_rels/.rels": relXML(),
|
||
"ppt/presentation.xml": `<?xml version="1.0"?><p:presentation xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:sldIdLst><p:sldId id="2"/></p:sldIdLst></p:presentation>`,
|
||
"ppt/_rels/presentation.xml.rels": relXML(relSlide),
|
||
"ppt/slides/slide1.xml": gateSlide,
|
||
"ppt/slideLayouts/slideLayout1.xml": gateLayout,
|
||
})
|
||
err := ooxmlStructuralCheck("事故一.pptx", broken)
|
||
if err == nil {
|
||
t.Fatal("incident-shaped pptx must be rejected")
|
||
}
|
||
for _, want := range []string{"母版", "sldMasterIdLst"} {
|
||
if !strings.Contains(err.Error(), want) {
|
||
t.Fatalf("error %q must mention %q", err, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 第二类事故:部件齐全但引用断裂——上一版门禁放行、Office 拒开的形态。
|
||
func TestOoxmlGateIncident2ReferenceBreaks(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
fixNS, fixRels, fixCT bool
|
||
want string
|
||
}{
|
||
{"undeclared-ns", false, true, true, "未声明的命名空间前缀 xmlns:r"},
|
||
{"sldid-dangling-rid", true, false, true, "sldIdLst 引用的 slide 在关系表中不存在"},
|
||
{"ct-missing-slides", true, true, false, "Content_Types"},
|
||
}
|
||
for _, tc := range cases {
|
||
err := ooxmlStructuralCheck("事故二.pptx", gateIncident2Pptx(t, tc.fixNS, tc.fixRels, tc.fixCT))
|
||
if err == nil {
|
||
t.Fatalf("%s: must be rejected", tc.name)
|
||
}
|
||
if !strings.Contains(err.Error(), tc.want) {
|
||
t.Fatalf("%s: error %q must mention %q", tc.name, err, tc.want)
|
||
}
|
||
}
|
||
// 三处缺陷全部修复后必须放行(对应 P4 修复产物的形态)。
|
||
if err := ooxmlStructuralCheck("修复后.pptx", gateIncident2Pptx(t, true, true, true)); err != nil {
|
||
t.Fatalf("fully repaired incident-2 package must pass: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestOoxmlGatePptxChainBreaks(t *testing.T) {
|
||
base := func(t *testing.T) map[string]string {
|
||
t.Helper()
|
||
valid := gateValidPptx(t)
|
||
zr, err := zip.NewReader(bytes.NewReader(valid), int64(len(valid)))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
m := map[string]string{}
|
||
for _, f := range zr.File {
|
||
b, err := readZipFile(f)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
m[f.Name] = string(b)
|
||
}
|
||
return m
|
||
}
|
||
cases := []struct {
|
||
name string
|
||
mut func(m map[string]string)
|
||
want string
|
||
}{
|
||
{"slide-no-rels", func(m map[string]string) {
|
||
delete(m, "ppt/slides/_rels/slide1.xml.rels")
|
||
}, "关系表缺失"},
|
||
{"slide-no-layout-rel", func(m map[string]string) {
|
||
m["ppt/slides/_rels/slide1.xml.rels"] = relXML()
|
||
}, "没有指向 slideLayout"},
|
||
{"layout-missing", func(m map[string]string) {
|
||
delete(m, "ppt/slideLayouts/slideLayout1.xml")
|
||
}, "缺少必备部件"},
|
||
{"layout-no-master-rel", func(m map[string]string) {
|
||
m["ppt/slideLayouts/_rels/slideLayout1.xml.rels"] = relXML()
|
||
}, "没有指向 slideMaster"},
|
||
{"layout-master-not-listed", func(m map[string]string) {
|
||
m["ppt/slideLayouts/_rels/slideLayout1.xml.rels"] = relXML(`<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="../slideMasters/slideMaster2.xml"/>`)
|
||
m["ppt/slideMasters/slideMaster2.xml"] = gateMaster
|
||
m["ppt/slideMasters/_rels/slideMaster2.xml.rels"] = relXML(relLayout, relTheme)
|
||
}, "未在 presentation 的 sldMasterIdLst 中声明"},
|
||
{"master-no-theme-rel", func(m map[string]string) {
|
||
m["ppt/slideMasters/_rels/slideMaster1.xml.rels"] = relXML(relLayout)
|
||
}, "没有指向 theme"},
|
||
{"theme-missing", func(m map[string]string) {
|
||
delete(m, "ppt/theme/theme1.xml")
|
||
}, "缺少必备部件"},
|
||
{"master-no-layout-idlst", func(m map[string]string) {
|
||
m["ppt/slideMasters/slideMaster1.xml"] = strings.Replace(gateMaster, `<p:sldLayoutIdLst><p:sldLayoutId id="2147483647" r:id="rId1"/></p:sldLayoutIdLst>`, "", 1)
|
||
}, "缺少 <sldLayoutIdLst>"},
|
||
{"pres-no-master-idlst", func(m map[string]string) {
|
||
m["ppt/presentation.xml"] = strings.Replace(
|
||
m["ppt/presentation.xml"],
|
||
`<p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="rId2"/></p:sldMasterIdLst>`, "", 1)
|
||
}, "sldMasterIdLst"},
|
||
{"pres-slide-rel-dangling", func(m map[string]string) {
|
||
m["ppt/_rels/presentation.xml.rels"] = relXML(relMaster)
|
||
}, "sldIdLst 引用的 slide 在关系表中不存在"},
|
||
{"pres-extra-slide-rel", func(m map[string]string) {
|
||
m["ppt/_rels/presentation.xml.rels"] = relXML(relSlide, relMaster,
|
||
`<Relationship Id="rId9" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide2.xml"/>`)
|
||
m["ppt/slides/slide2.xml"] = gateSlide
|
||
m["ppt/slides/_rels/slide2.xml.rels"] = relXML(relLayout)
|
||
}, "数量不一致"},
|
||
{"xml-malformed", func(m map[string]string) {
|
||
m["ppt/slides/slide1.xml"] = `<p:sld xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">`
|
||
}, "不是合法 XML"},
|
||
{"ct-slide-mismatch", func(m map[string]string) {
|
||
m["[Content_Types].xml"] = strings.Replace(gateCT(gateAllParts...), ctSlide, "application/xml", 1)
|
||
}, "内容类型不符"},
|
||
}
|
||
for _, tc := range cases {
|
||
m := base(t)
|
||
tc.mut(m)
|
||
err := ooxmlStructuralCheck("deck.pptx", zipOf(t, m))
|
||
if err == nil {
|
||
t.Fatalf("%s: must be rejected", tc.name)
|
||
}
|
||
if !strings.Contains(err.Error(), tc.want) {
|
||
t.Fatalf("%s: error %q must mention %q", tc.name, err, tc.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func readZipFile(f *zip.File) ([]byte, error) {
|
||
rc, err := f.Open()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rc.Close()
|
||
var buf bytes.Buffer
|
||
if _, err := buf.ReadFrom(rc); err != nil {
|
||
return nil, err
|
||
}
|
||
return buf.Bytes(), nil
|
||
}
|
||
|
||
func TestOoxmlGateDocxXlsxAndPassthrough(t *testing.T) {
|
||
validDocx := zipOf(t, map[string]string{
|
||
"[Content_Types].xml": gateCTXML,
|
||
"word/document.xml": `<?xml version="1.0"?><w:document xmlns:w="urn:x"/>`,
|
||
})
|
||
if err := ooxmlStructuralCheck("方案.docx", validDocx); err != nil {
|
||
t.Fatalf("valid docx must pass: %v", err)
|
||
}
|
||
if err := ooxmlStructuralCheck("方案.docx", zipOf(t, map[string]string{"[Content_Types].xml": gateCTXML})); err == nil {
|
||
t.Fatal("docx without document.xml must be rejected")
|
||
}
|
||
// 命名空间严格化同样覆盖 docx:正文用了未声明前缀要拦。
|
||
badDocx := zipOf(t, map[string]string{
|
||
"[Content_Types].xml": gateCTXML,
|
||
"word/document.xml": `<?xml version="1.0"?><w:document><w:p r:id="r1"/></w:document>`,
|
||
})
|
||
err := ooxmlStructuralCheck("方案.docx", badDocx)
|
||
if err == nil || !strings.Contains(err.Error(), "未声明") {
|
||
t.Fatalf("docx with undeclared prefix must be rejected: %v", err)
|
||
}
|
||
validXlsx := zipOf(t, map[string]string{
|
||
"[Content_Types].xml": gateCTXML,
|
||
"xl/workbook.xml": `<?xml version="1.0"?><workbook xmlns="urn:x"/>`,
|
||
})
|
||
if err := ooxmlStructuralCheck("数据.xlsx", validXlsx); err != nil {
|
||
t.Fatalf("valid xlsx must pass: %v", err)
|
||
}
|
||
if err := ooxmlStructuralCheck("备注.md", []byte("# 标题\n正文")); err != nil {
|
||
t.Fatalf("non-office files must pass through: %v", err)
|
||
}
|
||
if err := ooxmlStructuralCheck("坏.pptx", []byte("这是一段文本,不是 zip")); err == nil {
|
||
t.Fatal("text masquerading as pptx must be rejected")
|
||
}
|
||
}
|
||
|
||
// 登记口集成:残缺包在 register_artifact 处被拒并给出重建指引;合法包照常登记。
|
||
func TestRegisterArtifactStructuralGate(t *testing.T) {
|
||
r := NewRegistry()
|
||
if err := RegisterExecTools(r); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
root := t.TempDir()
|
||
rc := &execFake{root: root, owner: "o1", conv: "c1", execOn: true}
|
||
sandbox := filepath.Join(root, "o1", "c1")
|
||
if err := os.MkdirAll(sandbox, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
broken := filepath.Join(sandbox, "坏.pptx")
|
||
if err := os.WriteFile(broken, gateIncident2Pptx(t, false, false, false), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
res := interpCall(t, r, rc, "register_artifact", `{"path":"坏.pptx"}`)
|
||
if res.Status != "error" || !strings.Contains(res.Output, "结构自检未通过") || !strings.Contains(res.Output, "housestyle") {
|
||
t.Fatalf("broken pptx must be refused with rebuild hint: %s %s", res.Status, res.Output)
|
||
}
|
||
good := filepath.Join(sandbox, "好.pptx")
|
||
if err := os.WriteFile(good, gateValidPptx(t), 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
res = interpCall(t, r, rc, "register_artifact", `{"path":"好.pptx"}`)
|
||
if res.Status != "ok" || !strings.Contains(res.Output, "已登记交付物") {
|
||
t.Fatalf("valid pptx must register: %s %s", res.Status, res.Output)
|
||
}
|
||
}
|
||
|
||
func TestRegisterArtifactRejectsRenamedWordprocessingMLPackages(t *testing.T) {
|
||
r := NewRegistry()
|
||
if err := RegisterExecTools(r); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
root := t.TempDir()
|
||
rc := &execFake{root: root, owner: "o1", conv: "c1", execOn: true}
|
||
sandbox := filepath.Join(root, "o1", "c1")
|
||
if err := os.MkdirAll(sandbox, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
wordTypes := []struct {
|
||
name string
|
||
mainPart string
|
||
relType string
|
||
contentType string
|
||
}{
|
||
{"report.zip", "custom/main.xml", gateOfficeRel, "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"},
|
||
{"macro.bin", "word/document.xml", gateOfficeRel, "application/vnd.ms-word.document.macroEnabled.main+xml"},
|
||
{"template.data", "templates/body.xml", gateStrictOffice, "application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml"},
|
||
{"macro-template.payload", "package/document.xml", gateOfficeRel, "application/vnd.ms-word.template.macroEnabledTemplate.main+xml"},
|
||
}
|
||
for _, test := range wordTypes {
|
||
t.Run(test.name, func(t *testing.T) {
|
||
pkg := gateOfficePackage(t,
|
||
gateOfficeRelXML(test.relType, test.mainPart, ""),
|
||
gateOverrideCT(test.mainPart, test.contentType),
|
||
test.mainPart,
|
||
)
|
||
if err := os.WriteFile(filepath.Join(sandbox, test.name), pkg, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
res := interpCall(t, r, rc, "register_artifact", `{"path":`+mustJSON(t, test.name)+`}`)
|
||
if res.Status != "error" || !strings.Contains(res.Output, "direct Word registration is forbidden") {
|
||
t.Fatalf("renamed WordprocessingML package must be refused: %s %s", res.Status, res.Output)
|
||
}
|
||
})
|
||
}
|
||
|
||
defaultTyped := gateOfficePackage(t,
|
||
gateOfficeRelXML(gateOfficeRel, "alternate/main.wml", ""),
|
||
`<?xml version="1.0"?><Types xmlns="`+gateContentNS+`"><Default Extension="wml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>`,
|
||
"alternate/main.wml",
|
||
)
|
||
if err := os.WriteFile(filepath.Join(sandbox, "default-typed.pkg"), defaultTyped, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
res := interpCall(t, r, rc, "register_artifact", `{"path":"default-typed.pkg"}`)
|
||
if res.Status != "error" || !strings.Contains(res.Output, "direct Word registration is forbidden") {
|
||
t.Fatalf("default-typed WordprocessingML package must be refused: %s %s", res.Status, res.Output)
|
||
}
|
||
|
||
fakeWordPart := zipOf(t, map[string]string{
|
||
"word/document.xml": `<?xml version="1.0"?><not-word/>`,
|
||
"data/payload.json": `{"ok":true}`,
|
||
})
|
||
if err := os.WriteFile(filepath.Join(sandbox, "same-name.zip"), fakeWordPart, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
res = interpCall(t, r, rc, "register_artifact", `{"path":"same-name.zip"}`)
|
||
if res.Status != "ok" || !strings.Contains(res.Output, "已登记交付物") {
|
||
t.Fatalf("ordinary ZIP with a same-name part must register: %s %s", res.Status, res.Output)
|
||
}
|
||
|
||
spreadsheet := gateOfficePackage(t,
|
||
gateOfficeRelXML(gateOfficeRel, "xl/workbook.xml", ""),
|
||
gateOverrideCT("xl/workbook.xml", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"),
|
||
"xl/workbook.xml",
|
||
)
|
||
|
||
allowed := map[string][]byte{
|
||
"dataset.zip": zipOf(t, map[string]string{"data/payload.json": `{"ok":true}`}),
|
||
"spreadsheet.pkg": spreadsheet,
|
||
"payload.bin": {0x00, 0x01, 0x02, 0xff},
|
||
}
|
||
for name, content := range allowed {
|
||
if err := os.WriteFile(filepath.Join(sandbox, name), content, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
res := interpCall(t, r, rc, "register_artifact", `{"path":`+mustJSON(t, name)+`}`)
|
||
if res.Status != "ok" || !strings.Contains(res.Output, "已登记交付物") {
|
||
t.Fatalf("non-Word artifact %s must register: %s %s", name, res.Status, res.Output)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestRegisterArtifactRejectsMalformedAmbiguousOrUnsafeOfficeDocumentMetadata(t *testing.T) {
|
||
r := NewRegistry()
|
||
if err := RegisterExecTools(r); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
root := t.TempDir()
|
||
rc := &execFake{root: root, owner: "o1", conv: "c1", execOn: true}
|
||
sandbox := filepath.Join(root, "o1", "c1")
|
||
if err := os.MkdirAll(sandbox, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
const wordType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"
|
||
validCT := gateOverrideCT("custom/main.xml", wordType)
|
||
validRel := gateOfficeRelXML(gateOfficeRel, "custom/main.xml", "")
|
||
tests := []struct {
|
||
name string
|
||
rels string
|
||
contentTypes string
|
||
}{
|
||
{
|
||
name: "malformed-relationships.pkg",
|
||
rels: `<?xml version="1.0"?><Relationships xmlns="` + gatePackageRelNS + `"><Relationship Id="rId1" Type="` + gateOfficeRel + `" Target="custom/main.xml">`,
|
||
contentTypes: validCT,
|
||
},
|
||
{
|
||
name: "duplicate-office-relationships.pkg",
|
||
rels: relXML(
|
||
`<Relationship Id="rId1" Type="`+gateOfficeRel+`" Target="custom/main.xml"/>`,
|
||
`<Relationship Id="rId2" Type="`+gateOfficeRel+`" Target="other/main.xml"/>`,
|
||
),
|
||
contentTypes: validCT,
|
||
},
|
||
{
|
||
name: "malformed-content-types.pkg",
|
||
rels: validRel,
|
||
contentTypes: `<?xml version="1.0"?><Types xmlns="` + gateContentNS + `"><Override PartName="/custom/main.xml" ContentType="` + wordType + `">`,
|
||
},
|
||
{
|
||
name: "duplicate-content-type.pkg",
|
||
rels: validRel,
|
||
contentTypes: `<?xml version="1.0"?><Types xmlns="` + gateContentNS + `">` +
|
||
`<Override PartName="/custom/main.xml" ContentType="` + wordType + `"/>` +
|
||
`<Override PartName="/custom/main.xml" ContentType="application/xml"/></Types>`,
|
||
},
|
||
{
|
||
name: "missing-main-content-type.pkg",
|
||
rels: validRel,
|
||
contentTypes: `<?xml version="1.0"?><Types xmlns="` + gateContentNS + `"></Types>`,
|
||
},
|
||
{
|
||
name: "traversal-target.pkg",
|
||
rels: gateOfficeRelXML(gateOfficeRel, "../custom/main.xml", ""),
|
||
contentTypes: validCT,
|
||
},
|
||
{
|
||
name: "external-target.pkg",
|
||
rels: gateOfficeRelXML(gateOfficeRel, "https://example.invalid/main.xml", "External"),
|
||
contentTypes: validCT,
|
||
},
|
||
}
|
||
for _, test := range tests {
|
||
t.Run(test.name, func(t *testing.T) {
|
||
pkg := gateOfficePackage(t, test.rels, test.contentTypes, "custom/main.xml")
|
||
if err := os.WriteFile(filepath.Join(sandbox, test.name), pkg, 0o644); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
res := interpCall(t, r, rc, "register_artifact", `{"path":`+mustJSON(t, test.name)+`}`)
|
||
if res.Status != "error" || !strings.Contains(res.Output, "direct Word registration is forbidden") {
|
||
t.Fatalf("unsafe officeDocument metadata must fail closed: %s %s", res.Status, res.Output)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func mustJSON(t *testing.T, value string) string {
|
||
t.Helper()
|
||
raw, err := json.Marshal(value)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return string(raw)
|
||
}
|