aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/api_helpers.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 01:50:17 -0400
committerhistoria <[not public]>2026-09-18 01:50:17 -0400
commit87d08f0151e643951769e323995f1c5ed89ee200 (patch)
treedc42a0b30ebb5a00dd7ba505cc4eaa44f4f308bc /internal/admin/api_helpers.go
parent32a3c59cebf2a16277e54d715a67adfdbe00b456 (diff)
downloadthehouseoficarus-87d08f0151e643951769e323995f1c5ed89ee200.tar.gz
fix: harden economy recovery, shutdown concurrency, world validation, and inventory-loss paths
Diffstat (limited to 'internal/admin/api_helpers.go')
-rw-r--r--internal/admin/api_helpers.go46
1 files changed, 44 insertions, 2 deletions
diff --git a/internal/admin/api_helpers.go b/internal/admin/api_helpers.go
index 11ad958..37cc4ae 100644
--- a/internal/admin/api_helpers.go
+++ b/internal/admin/api_helpers.go
@@ -3,10 +3,13 @@ package admin
import (
"bytes"
"fmt"
- "os"
+ "path/filepath"
"strings"
"gopkg.in/yaml.v3"
+
+ "thehouseoficarus/internal/persistence"
+ "thehouseoficarus/internal/world"
)
// validEntityID reports whether an entity ID can be safely joined into a
@@ -23,6 +26,45 @@ func validEntityID(id string) bool {
return true
}
+// resolveRoomSubdir validates a client-supplied room subdirectory and returns
+// the cleaned path relative to <dataDir>/rooms. Absolute paths, ".." escapes,
+// drive-like prefixes, and backslashes are rejected; the empty string is the
+// rooms root. Room-directory API handlers must route body.Dir/patch.Dir through
+// this before joining, mirroring validEntityID/resolveDataPath elsewhere.
+func (s *AdminServer) resolveRoomSubdir(dir string) (string, error) {
+ dir = strings.TrimSpace(dir)
+ if dir == "" {
+ return "", nil
+ }
+ if filepath.IsAbs(dir) || strings.ContainsRune(dir, '\\') || strings.ContainsRune(dir, 0) {
+ return "", fmt.Errorf("invalid directory")
+ }
+ clean := filepath.Clean(dir)
+ if clean == "." {
+ return "", nil
+ }
+ if clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
+ return "", fmt.Errorf("directory escapes the rooms tree")
+ }
+ base := filepath.Join(s.dataDir, "rooms")
+ rel, err := filepath.Rel(base, filepath.Join(base, clean))
+ if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
+ return "", fmt.Errorf("directory escapes the rooms tree")
+ }
+ return clean, nil
+}
+
+// validateRoomSchema decodes a rendered room YAML into world.Room before it is
+// written, so the editor cannot persist a room the runtime loader would later
+// reject (for example a non-numeric exit target). KnownFields(true) also
+// catches keys the runtime would silently ignore.
+func validateRoomSchema(content []byte) error {
+ dec := yaml.NewDecoder(bytes.NewReader(content))
+ dec.KnownFields(true)
+ var room world.Room
+ return dec.Decode(&room)
+}
+
func yamlToMap(data []byte) (map[string]any, error) {
var v any
if err := yaml.Unmarshal(data, &v); err != nil {
@@ -65,7 +107,7 @@ func writeMapAsYAML(path string, m map[string]any) ([]byte, error) {
if err != nil {
return nil, err
}
- if err := os.WriteFile(path, content, 0644); err != nil {
+ if err := persistence.WriteFile(path, content, 0644); err != nil {
return nil, err
}
return content, nil