Fix paths asset regression

This commit is contained in:
2026-05-08 00:23:41 +02:00
parent 4dc16793cd
commit 9f16aa8872
4 changed files with 235 additions and 20 deletions
+47 -4
View File
@@ -156,10 +156,15 @@ func extractArchiveResources(p *project.Project, archive erf.Archive, desired ma
func extractedFile(p *project.Project, resource erf.Resource, extension string) (string, []byte, error) { func extractedFile(p *project.Project, resource erf.Resource, extension string) (string, []byte, error) {
resref := strings.ToLower(resource.Name) resref := strings.ToLower(resource.Name)
effective := p.EffectiveConfig()
switch extension { switch extension {
case "nss": case "nss":
return filepath.Join(p.ScriptSourceDir(), resref+".nss"), resource.Data, nil target, err := extractionTarget(p, "paths.source", effective.Paths.Source, p.SourceDir(), filepath.FromSlash(effective.Scripts.SourceDir), resref+".nss")
if err != nil {
return "", nil, err
}
return target, resource.Data, nil
case "utc", "utd", "ute", "uti", "utm", "utp", "uts", "utt", "utw", case "utc", "utd", "ute", "uti", "utm", "utp", "uts", "utt", "utw",
"are", "dlg", "fac", "gic", "git", "ifo", "itp", "jrl": "are", "dlg", "fac", "gic", "git", "ifo", "itp", "jrl":
document, err := gff.Read(bytes.NewReader(resource.Data)) document, err := gff.Read(bytes.NewReader(resource.Data))
@@ -171,10 +176,39 @@ func extractedFile(p *project.Project, resource erf.Resource, extension string)
return "", nil, fmt.Errorf("marshal json %s.%s: %w", resource.Name, extension, err) return "", nil, fmt.Errorf("marshal json %s.%s: %w", resource.Name, extension, err)
} }
formatted = append(formatted, '\n') formatted = append(formatted, '\n')
return filepath.Join(p.SourceDir(), sourceSubdir(extension), resref+"."+extension+".json"), formatted, nil target, err := extractionTarget(p, "paths.source", effective.Paths.Source, p.SourceDir(), sourceSubdir(extension), resref+"."+extension+".json")
default: if err != nil {
return filepath.Join(p.AssetsDir(), extension, resref+"."+extension), resource.Data, nil return "", nil, err
} }
return target, formatted, nil
default:
target, err := extractionTarget(p, "paths.assets", effective.Paths.Assets, p.AssetsDir(), extension, resref+"."+extension)
if err != nil {
return "", nil, err
}
return target, resource.Data, nil
}
}
func extractionTarget(p *project.Project, field, configured, root string, parts ...string) (string, error) {
if strings.TrimSpace(configured) == "" {
return "", fmt.Errorf("cannot extract resource: %s is not configured", field)
}
cleanRoot := filepath.Clean(root)
if cleanRoot == "." || cleanRoot == string(filepath.Separator) || cleanRoot == filepath.Clean(p.Root) {
return "", fmt.Errorf("cannot extract resource: %s resolves to unsafe extraction root %s", field, cleanRoot)
}
target := filepath.Join(append([]string{cleanRoot}, parts...)...)
rel, err := filepath.Rel(cleanRoot, target)
if err != nil {
return "", fmt.Errorf("resolve extraction target %s: %w", target, err)
}
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) || filepath.IsAbs(rel) {
return "", fmt.Errorf("refusing to extract resource outside %s: %s", cleanRoot, target)
}
return target, nil
} }
func extractHAKPaths(p *project.Project) ([]string, error) { func extractHAKPaths(p *project.Project) ([]string, error) {
@@ -236,15 +270,19 @@ func writeManagedFile(path string, data []byte) (writeState, error) {
func cleanupStaleFiles(p *project.Project, desired map[string]struct{}) (int, []error) { func cleanupStaleFiles(p *project.Project, desired map[string]struct{}) (int, []error) {
candidates := make([]string, 0, len(p.Inventory.SourceFiles)+len(p.Inventory.ScriptFiles)+len(p.Inventory.AssetFiles)) candidates := make([]string, 0, len(p.Inventory.SourceFiles)+len(p.Inventory.ScriptFiles)+len(p.Inventory.AssetFiles))
if safeCleanupRoot(p.SourceDir(), p.Root) {
for _, rel := range p.Inventory.SourceFiles { for _, rel := range p.Inventory.SourceFiles {
candidates = append(candidates, filepath.Join(p.SourceDir(), filepath.FromSlash(rel))) candidates = append(candidates, filepath.Join(p.SourceDir(), filepath.FromSlash(rel)))
} }
for _, rel := range p.Inventory.ScriptFiles { for _, rel := range p.Inventory.ScriptFiles {
candidates = append(candidates, filepath.Join(p.SourceDir(), filepath.FromSlash(rel))) candidates = append(candidates, filepath.Join(p.SourceDir(), filepath.FromSlash(rel)))
} }
}
if safeCleanupRoot(p.AssetsDir(), p.Root) {
for _, rel := range p.Inventory.AssetFiles { for _, rel := range p.Inventory.AssetFiles {
candidates = append(candidates, filepath.Join(p.AssetsDir(), filepath.FromSlash(rel))) candidates = append(candidates, filepath.Join(p.AssetsDir(), filepath.FromSlash(rel)))
} }
}
removed := 0 removed := 0
var failures []error var failures []error
@@ -266,6 +304,11 @@ func cleanupStaleFiles(p *project.Project, desired map[string]struct{}) (int, []
return removed, failures return removed, failures
} }
func safeCleanupRoot(root, projectRoot string) bool {
cleanRoot := filepath.Clean(root)
return cleanRoot != "." && cleanRoot != string(filepath.Separator) && cleanRoot != filepath.Clean(projectRoot)
}
func cleanupEmptyParents(dir string, roots ...string) { func cleanupEmptyParents(dir string, roots ...string) {
for { for {
if dir == "." || dir == string(filepath.Separator) { if dir == "." || dir == string(filepath.Separator) {
+167
View File
@@ -175,6 +175,173 @@ func TestExtractReadsHAKAssets(t *testing.T) {
} }
} }
func TestExtractRefusesAssetsWhenAssetsPathIsUnset(t *testing.T) {
root := t.TempDir()
t.Chdir(root)
mustMkdir(t, filepath.Join(root, "src"))
mustMkdir(t, filepath.Join(root, "build"))
mustWriteFile(t, filepath.Join(root, "root_level_asset.mdl"), "must stay")
mustWriteFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
name: Test Module
resref: testmod
paths:
source: src
build: build
`)
p, err := project.Load(root)
if err != nil {
t.Fatalf("load project: %v", err)
}
if err := p.Scan(); err != nil {
t.Fatalf("scan: %v", err)
}
if len(p.Inventory.AssetFiles) != 0 {
t.Fatalf("expected unset assets path not to inventory repository-root assets, got %#v", p.Inventory.AssetFiles)
}
modFile, err := os.Create(p.ModuleArchivePath())
if err != nil {
t.Fatalf("create mod: %v", err)
}
if err := erf.Write(modFile, erf.New("MOD ", nil)); err != nil {
t.Fatalf("write mod: %v", err)
}
if err := modFile.Close(); err != nil {
t.Fatalf("close mod: %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)
}
_, err = Extract(p)
if err == nil {
t.Fatal("expected extract to fail")
}
if !strings.Contains(err.Error(), "paths.assets is not configured") {
t.Fatalf("expected missing assets path error, got %v", err)
}
if _, statErr := os.Stat(filepath.Join(root, "mdl", "test_vfx.mdl")); !os.IsNotExist(statErr) {
t.Fatalf("expected no root-level extracted asset, stat err=%v", statErr)
}
if _, statErr := os.Stat(filepath.Join(root, "root_level_asset.mdl")); statErr != nil {
t.Fatalf("expected pre-existing root-level asset to remain, stat err=%v", statErr)
}
}
func TestExtractRefusesAssetsWhenAssetsPathIsRepositoryRoot(t *testing.T) {
root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src"))
mustMkdir(t, filepath.Join(root, "build"))
mustWriteFile(t, filepath.Join(root, "nwn-tool.yaml"), `
module:
name: Test Module
resref: testmod
paths:
source: src
assets: .
build: build
`)
p, err := project.Load(root)
if err != nil {
t.Fatalf("load project: %v", err)
}
modFile, err := os.Create(p.ModuleArchivePath())
if err != nil {
t.Fatalf("create mod: %v", err)
}
if err := erf.Write(modFile, erf.New("MOD ", nil)); err != nil {
t.Fatalf("write mod: %v", err)
}
if err := modFile.Close(); err != nil {
t.Fatalf("close mod: %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)
}
_, err = Extract(p)
if err == nil {
t.Fatal("expected extract to fail")
}
if !strings.Contains(err.Error(), "paths.assets resolves to unsafe extraction root") {
t.Fatalf("expected unsafe assets root error, got %v", err)
}
if _, statErr := os.Stat(filepath.Join(root, "mdl", "test_vfx.mdl")); !os.IsNotExist(statErr) {
t.Fatalf("expected no root-level extracted asset, stat err=%v", statErr)
}
}
func TestExtractRefusesSourceWhenSourcePathIsUnset(t *testing.T) {
root := t.TempDir()
t.Chdir(root)
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:
assets: assets
build: build
`)
p, err := project.Load(root)
if err != nil {
t.Fatalf("load project: %v", err)
}
modFile, err := os.Create(p.ModuleArchivePath())
if err != nil {
t.Fatalf("create mod: %v", err)
}
if err := erf.Write(modFile, erf.New("MOD ", []erf.Resource{
{Name: "test_script", Type: 0x07D9, Data: []byte("void main() {}\n")},
})); err != nil {
t.Fatalf("write mod: %v", err)
}
if err := modFile.Close(); err != nil {
t.Fatalf("close mod: %v", err)
}
_, err = Extract(p)
if err == nil {
t.Fatal("expected extract to fail")
}
if !strings.Contains(err.Error(), "paths.source is not configured") {
t.Fatalf("expected missing source path error, got %v", err)
}
if _, statErr := os.Stat(filepath.Join(root, "scripts", "test_script.nss")); !os.IsNotExist(statErr) {
t.Fatalf("expected no root-level extracted script, stat err=%v", statErr)
}
}
func TestExtractConfiguredHAKDiscoveryIgnoresUnconfiguredArchives(t *testing.T) { func TestExtractConfiguredHAKDiscoveryIgnoresUnconfiguredArchives(t *testing.T) {
root := t.TempDir() root := t.TempDir()
mustMkdir(t, filepath.Join(root, "src")) mustMkdir(t, filepath.Join(root, "src"))
+7 -2
View File
@@ -581,7 +581,7 @@ func (p *Project) Scan() error {
effective := p.EffectiveConfig() effective := p.EffectiveConfig()
sourceDir := p.SourceDir() sourceDir := p.SourceDir()
if sourceDir != "" && filepath.Clean(sourceDir) != p.Root { if sourceDir != "" && filepath.Clean(sourceDir) != filepath.Clean(p.Root) {
var err error var err error
sourceFiles, sourceExts, err = scanDir(sourceDir, func(path string) bool { sourceFiles, sourceExts, err = scanDir(sourceDir, func(path string) bool {
ext := strings.ToLower(filepath.Ext(path)) ext := strings.ToLower(filepath.Ext(path))
@@ -594,7 +594,11 @@ func (p *Project) Scan() error {
} }
} }
assetFiles, _, err := scanDir(p.AssetsDir(), func(path string) bool { assetDir := p.AssetsDir()
var assetFiles []string
if assetDir != "" && filepath.Clean(assetDir) != filepath.Clean(p.Root) {
var err error
assetFiles, _, err = scanDir(assetDir, func(path string) bool {
ext := strings.ToLower(filepath.Ext(path)) ext := strings.ToLower(filepath.Ext(path))
return slices.Contains(effective.Inventory.AssetExtensions, ext) return slices.Contains(effective.Inventory.AssetExtensions, ext)
}) })
@@ -605,6 +609,7 @@ func (p *Project) Scan() error {
return fmt.Errorf("scan assets tree: %w", err) return fmt.Errorf("scan assets tree: %w", err)
} }
} }
}
var scripts []string var scripts []string
var sources []string var sources []string
BIN
View File
Binary file not shown.