Extraction Hardening

This commit is contained in:
2026-05-08 01:26:41 +02:00
parent fa4c9116ae
commit 91c90793a2
7 changed files with 515 additions and 88 deletions
+282 -3
View File
@@ -120,6 +120,9 @@ func TestExtractReadsHAKAssets(t *testing.T) {
"source": "src",
"assets": "assets",
"build": "build"
},
"extract": {
"archives": ["*.hak"]
}
}
`)
@@ -175,6 +178,281 @@ func TestExtractReadsHAKAssets(t *testing.T) {
}
}
func TestExtractDefaultOnlyReadsConfiguredModuleArchive(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src", "module"))
mustMkdir(t, filepath.Join(root, "assets", "mdl"))
mustMkdir(t, filepath.Join(root, "build"))
mustWriteFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
name: Test Module
resref: testmod
paths:
source: src
assets: assets
build: build
`)
mustWriteFile(t, filepath.Join(root, "src", "module", "module.ifo.json"), `{
"file_type": "IFO ",
"file_version": "V3.2",
"root": {
"struct_type": 0,
"fields": [
{
"label": "Mod_Name",
"type": "CExoString",
"value": "Original Module"
}
]
}
}
`)
mustWriteFile(t, filepath.Join(root, "assets", "mdl", "stale_asset.mdl"), "keep")
p, err := project.Load(root)
if err != nil {
t.Fatalf("load project: %v", err)
}
if err := p.ValidateLayout(); err != nil {
t.Fatalf("validate layout: %v", err)
}
if err := p.Scan(); err != nil {
t.Fatalf("scan: %v", err)
}
if _, err := BuildModule(p); err != nil {
t.Fatalf("build module: %v", err)
}
if err := os.Remove(filepath.Join(root, "src", "module", "module.ifo.json")); err != nil {
t.Fatalf("remove source file: %v", err)
}
if err := p.Scan(); err != nil {
t.Fatalf("rescan before extract: %v", err)
}
hakFile, err := os.Create(filepath.Join(root, "build", "vfx.hak"))
if err != nil {
t.Fatalf("create hak: %v", err)
}
if err := erf.Write(hakFile, erf.New("HAK ", []erf.Resource{
{Name: "test_vfx", Type: 0x07D2, Data: []byte("mdl-data")},
})); err != nil {
t.Fatalf("write hak: %v", err)
}
if err := hakFile.Close(); err != nil {
t.Fatalf("close hak: %v", err)
}
result, err := Extract(p)
if err != nil {
t.Fatalf("extract: %v", err)
}
if len(result.HAKPaths) != 0 {
t.Fatalf("expected default extract to ignore haks, got %#v", result.HAKPaths)
}
if _, err := os.Stat(filepath.Join(root, "src", "module", "module.ifo.json")); err != nil {
t.Fatalf("expected module source to be restored: %v", err)
}
if _, err := os.Stat(filepath.Join(root, "assets", "mdl", "stale_asset.mdl")); err != nil {
t.Fatalf("expected asset cleanup to be skipped for module-only extract: %v", err)
}
if _, err := os.Stat(filepath.Join(root, "assets", "mdl", "test_vfx.mdl")); !os.IsNotExist(err) {
t.Fatalf("expected hak asset not to be extracted by default, err=%v", err)
}
}
func TestExtractRejectsERFArchiveTargets(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src"))
mustMkdir(t, filepath.Join(root, "assets"))
mustMkdir(t, filepath.Join(root, "build"))
mustWriteFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
name: Test Module
resref: testmod
paths:
source: src
assets: assets
build: build
extract:
archives:
- imported.erf
`)
erfFile, err := os.Create(filepath.Join(root, "build", "imported.erf"))
if err != nil {
t.Fatalf("create erf: %v", err)
}
if err := erf.Write(erfFile, erf.New("ERF ", nil)); err != nil {
t.Fatalf("write erf: %v", err)
}
if err := erfFile.Close(); err != nil {
t.Fatalf("close erf: %v", err)
}
p, err := project.Load(root)
if err != nil {
t.Fatalf("load project: %v", err)
}
_, err = Extract(p)
if err == nil {
t.Fatal("expected erf extraction to fail")
}
if !strings.Contains(err.Error(), "ERF extraction is unsafe") {
t.Fatalf("expected unsafe erf error, got %v", err)
}
}
func TestExtractRejectsEscapingArchiveOverride(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src"))
mustMkdir(t, filepath.Join(root, "assets"))
mustMkdir(t, filepath.Join(root, "build"))
mustWriteFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
name: Test Module
resref: testmod
paths:
source: src
assets: assets
build: build
`)
p, err := project.Load(root)
if err != nil {
t.Fatalf("load project: %v", err)
}
_, err = Extract(p, "../outside.mod")
if err == nil {
t.Fatal("expected escaping archive override to fail")
}
if !strings.Contains(err.Error(), "must not escape paths.build") {
t.Fatalf("expected escaping archive error, got %v", err)
}
}
func TestExtractConsumesConfiguredHAKArchiveAfterSuccess(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src"))
mustMkdir(t, filepath.Join(root, "assets"))
mustMkdir(t, filepath.Join(root, "build"))
mustWriteFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
name: Test Module
resref: testmod
paths:
source: src
assets: assets
build: build
extract:
archives:
- vfx.hak
consume_archives: true
`)
hakPath := filepath.Join(root, "build", "vfx.hak")
hakFile, err := os.Create(hakPath)
if err != nil {
t.Fatalf("create hak: %v", err)
}
if err := erf.Write(hakFile, erf.New("HAK ", []erf.Resource{
{Name: "test_vfx", Type: 0x07D2, Data: []byte("mdl-data")},
})); err != nil {
t.Fatalf("write hak: %v", err)
}
if err := hakFile.Close(); err != nil {
t.Fatalf("close hak: %v", err)
}
p, err := project.Load(root)
if err != nil {
t.Fatalf("load project: %v", err)
}
result, err := Extract(p)
if err != nil {
t.Fatalf("extract: %v", err)
}
if len(result.DeletedArchivePaths) != 1 || result.DeletedArchivePaths[0] != hakPath {
t.Fatalf("expected consumed hak path, got %#v", result.DeletedArchivePaths)
}
if _, err := os.Stat(hakPath); !os.IsNotExist(err) {
t.Fatalf("expected hak archive to be deleted, stat err=%v", err)
}
if _, err := os.Stat(filepath.Join(root, "assets", "mdl", "test_vfx.mdl")); err != nil {
t.Fatalf("expected extracted hak asset: %v", err)
}
}
func TestExtractConsumeArchivesFalseOverridesLegacyDeleteFlag(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src", "module"))
mustMkdir(t, filepath.Join(root, "assets"))
mustMkdir(t, filepath.Join(root, "build"))
mustWriteFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
name: Test Module
resref: testmod
paths:
source: src
assets: assets
build: build
extract:
consume_archives: false
delete_module_archive_after_success: true
`)
mustWriteFile(t, filepath.Join(root, "src", "module", "module.ifo.json"), `{
"file_type": "IFO ",
"file_version": "V3.2",
"root": {
"struct_type": 0,
"fields": [
{
"label": "Mod_Name",
"type": "CExoString",
"value": "Original Module"
}
]
}
}
`)
p, err := project.Load(root)
if err != nil {
t.Fatalf("load project: %v", err)
}
if err := p.ValidateLayout(); err != nil {
t.Fatalf("validate layout: %v", err)
}
if err := p.Scan(); err != nil {
t.Fatalf("scan: %v", err)
}
buildResult, err := BuildModule(p)
if err != nil {
t.Fatalf("build module: %v", err)
}
if err := os.Remove(filepath.Join(root, "src", "module", "module.ifo.json")); err != nil {
t.Fatalf("remove source file: %v", err)
}
if err := p.Scan(); err != nil {
t.Fatalf("rescan before extract: %v", err)
}
result, err := Extract(p)
if err != nil {
t.Fatalf("extract: %v", err)
}
if len(result.DeletedArchivePaths) != 0 {
t.Fatalf("expected no deleted archives, got %#v", result.DeletedArchivePaths)
}
if _, err := os.Stat(buildResult.ModulePath); err != nil {
t.Fatalf("expected module archive to remain, stat err=%v", err)
}
}
func TestExtractRefusesAssetsWhenAssetsPathIsUnset(t *testing.T) {
root := t.TempDir()
t.Chdir(root)
@@ -226,7 +504,7 @@ paths:
t.Fatalf("close hak: %v", err)
}
_, err = Extract(p)
_, err = Extract(p, "vfx.hak")
if err == nil {
t.Fatal("expected extract to fail")
}
@@ -285,7 +563,7 @@ paths:
t.Fatalf("close hak: %v", err)
}
_, err = Extract(p)
_, err = Extract(p, "vfx.hak")
if err == nil {
t.Fatal("expected extract to fail")
}
@@ -357,7 +635,8 @@ paths:
assets: assets
build: build
extract:
hak_discovery: configured_haks
archives:
- wanted.hak
haks:
- name: wanted
priority: 1