diff --git a/internal/app/operator_inspection.go b/internal/app/operator_inspection.go index 0a05d6d..60a4266 100644 --- a/internal/app/operator_inspection.go +++ b/internal/app/operator_inspection.go @@ -276,8 +276,8 @@ func requireInspectionFile(path, label string) error { } return fmt.Errorf("stat %s %q: %w", label, path, err) } - if info.IsDir() { - return fmt.Errorf("%s %q is a directory", label, path) + if !info.Mode().IsRegular() { + return fmt.Errorf("%s %q is not a regular file", label, path) } return nil } diff --git a/internal/app/operator_inspection_fifo_test.go b/internal/app/operator_inspection_fifo_test.go new file mode 100644 index 0000000..923ece6 --- /dev/null +++ b/internal/app/operator_inspection_fifo_test.go @@ -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") +} diff --git a/internal/stage/prepare.go b/internal/stage/prepare.go index ec6f33b..ccce860 100644 --- a/internal/stage/prepare.go +++ b/internal/stage/prepare.go @@ -635,6 +635,13 @@ func requireRegularReadableFile(path string, label string) error { if strings.TrimSpace(path) == "" { 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) if err != nil { return fmt.Errorf("open %s %q: %w", label, path, err) @@ -644,8 +651,13 @@ func requireRegularReadableFile(path string, label string) error { if statErr != nil { return fmt.Errorf("stat %s %q: %w", label, path, statErr) } - if !info.Mode().IsRegular() { - return fmt.Errorf("%s %q is not a regular file", label, path) + current, currentErr := os.Lstat(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 { return fmt.Errorf("close %s %q: %w", label, path, closeErr) diff --git a/internal/stage/prepare_fifo_test.go b/internal/stage/prepare_fifo_test.go new file mode 100644 index 0000000..6519ca5 --- /dev/null +++ b/internal/stage/prepare_fifo_test.go @@ -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") + } +}