Reject special files before preparing inputs

This commit is contained in:
2026-08-29 16:13:36 +00:00
parent a68e8e31a4
commit e7e3bef1e4
4 changed files with 95 additions and 4 deletions

View File

@@ -276,8 +276,8 @@ func requireInspectionFile(path, label string) error {
} }
return fmt.Errorf("stat %s %q: %w", label, path, err) return fmt.Errorf("stat %s %q: %w", label, path, err)
} }
if info.IsDir() { if !info.Mode().IsRegular() {
return fmt.Errorf("%s %q is a directory", label, path) return fmt.Errorf("%s %q is not a regular file", label, path)
} }
return nil return nil
} }

View File

@@ -0,0 +1,34 @@
//go:build unix
package app
import (
"path/filepath"
"strings"
"testing"
"gitea.maximumdirect.net/eric/narratio/internal/config"
"golang.org/x/sys/unix"
)
func TestInspectStableInputsRejectsSpellCatalogFIFO(t *testing.T) {
root := t.TempDir()
sourcePath := filepath.Join(root, "spells.fifo")
if err := unix.Mkfifo(sourcePath, 0o644); err != nil {
t.Fatalf("Mkfifo() error = %v", err)
}
cfg := &config.Config{StableInputs: config.ResolvedStableInputs{
SpellCatalogFile: config.ResolvedInputFile{Path: sourcePath, ConfigPath: filepath.Join(root, "campaign.yml")},
}}
for _, check := range inspectStableInputs(cfg) {
if check.Name != "spell_catalog" {
continue
}
if check.Err == nil || !strings.Contains(check.Err.Error(), "not a regular file") {
t.Fatalf("spell catalog check = %#v, want regular-file rejection", check)
}
return
}
t.Fatal("spell catalog inspection result was not reported")
}

View File

@@ -635,6 +635,13 @@ func requireRegularReadableFile(path string, label string) error {
if strings.TrimSpace(path) == "" { if strings.TrimSpace(path) == "" {
return fmt.Errorf("%s path is required", label) return fmt.Errorf("%s path is required", label)
} }
declared, err := os.Lstat(path)
if err != nil {
return fmt.Errorf("inspect %s %q: %w", label, path, err)
}
if declared.Mode()&os.ModeSymlink != 0 || !declared.Mode().IsRegular() {
return fmt.Errorf("%s %q is not a regular file without symlinks", label, path)
}
file, err := os.Open(path) file, err := os.Open(path)
if err != nil { if err != nil {
return fmt.Errorf("open %s %q: %w", label, path, err) return fmt.Errorf("open %s %q: %w", label, path, err)
@@ -644,8 +651,13 @@ func requireRegularReadableFile(path string, label string) error {
if statErr != nil { if statErr != nil {
return fmt.Errorf("stat %s %q: %w", label, path, statErr) return fmt.Errorf("stat %s %q: %w", label, path, statErr)
} }
if !info.Mode().IsRegular() { current, currentErr := os.Lstat(path)
return fmt.Errorf("%s %q is not a regular file", label, path) if currentErr != nil {
return fmt.Errorf("reinspect %s %q: %w", label, path, currentErr)
}
if !info.Mode().IsRegular() || current.Mode()&os.ModeSymlink != 0 || !current.Mode().IsRegular() ||
!os.SameFile(info, declared) || !os.SameFile(info, current) {
return fmt.Errorf("%s %q changed while being opened", label, path)
} }
if closeErr != nil { if closeErr != nil {
return fmt.Errorf("close %s %q: %w", label, path, closeErr) return fmt.Errorf("close %s %q: %w", label, path, closeErr)

View File

@@ -0,0 +1,45 @@
//go:build unix
package stage
import (
"context"
"path/filepath"
"strings"
"testing"
"time"
"gitea.maximumdirect.net/eric/narratio/internal/config"
"golang.org/x/sys/unix"
)
func TestPrepareStageRejectsSpellCatalogFIFOWithoutBlocking(t *testing.T) {
env, m := setupPrepareEnv(t)
root := filepath.Dir(env.Config.SessionPath)
writeFile(t, filepath.Join(root, "audio", "a.flac"), "audio")
env.Config.Session.Inputs.AudioFiles = []string{"./audio/a.flac"}
sourcePath := filepath.Join(root, "spells.fifo")
if err := unix.Mkfifo(sourcePath, 0o644); err != nil {
t.Fatalf("Mkfifo() error = %v", err)
}
env.Config.StableInputs.SpellCatalogFile = config.ResolvedInputFile{
Path: "./spells.fifo",
ConfigPath: env.Config.CampaignPath,
Source: "campaign_config",
}
done := make(chan error, 1)
go func() {
_, err := (prepareStage{}).Run(context.Background(), env, m)
done <- err
}()
select {
case err := <-done:
if err == nil || !strings.Contains(err.Error(), "not a regular file") {
t.Fatalf("prepare.Run() error = %v, want regular-file rejection", err)
}
case <-time.After(time.Second):
t.Fatal("prepare.Run() blocked while inspecting a FIFO spell catalog")
}
}