diff options
| author | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
| commit | 153acb7f644d755c45533b7c5311e3ffa47e8973 (patch) | |
| tree | 7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/player/store.go | |
| parent | 87d08f0151e643951769e323995f1c5ed89ee200 (diff) | |
| download | thehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz | |
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'internal/player/store.go')
| -rw-r--r-- | internal/player/store.go | 44 |
1 files changed, 42 insertions, 2 deletions
diff --git a/internal/player/store.go b/internal/player/store.go index d461485..4c8dce5 100644 --- a/internal/player/store.go +++ b/internal/player/store.go @@ -41,7 +41,18 @@ func (s *AccountStore) SaveAccount(acc *Account) error { if err != nil { return err } - return persistence.WriteFile(s.AccountPath(acc.Name), data, 0600) + return writePrivate(s.AccountPath(acc.Name), data) +} + +// writePrivate writes data atomically and then forces 0600. persistence.WriteFile +// deliberately preserves an existing file's mode, so an account/character file +// created before this rule (or by an external tool) would otherwise stay +// world-readable through every later save and password rehash. +func writePrivate(path string, data []byte) error { + if err := persistence.WriteFile(path, data, 0600); err != nil { + return err + } + return os.Chmod(path, 0600) } // MarshalAccount serializes an account; callers that own the account (the @@ -78,6 +89,10 @@ func (s *AccountStore) FindAccount(name string) (string, error) { return "", fmt.Errorf("account not found: %s", name) } +// characterInventorySlots is the on-disk inventory capacity, matching +// Player.FreeSlots and Player.FirstFreeSlot in player.go. +const characterInventorySlots = MaxInventorySlots + func (s *AccountStore) CharPath(name string) string { return filepath.Join(s.dataDir, "players", "characters", name+".yaml") } @@ -101,18 +116,43 @@ func (s *AccountStore) LoadCharacter(name string) (*Player, error) { if p.Inventory == nil { p.Inventory = make(map[int]*InventorySlot) } + sanitizeInventory(p.Inventory) + sanitizeBank(p.Bank) if p.MapSymbols == nil { p.MapSymbols = make(map[int]MapSymbolData) } return &p, nil } +// sanitizeInventory drops inventory entries a hand-edited or corrupt save can +// carry that no gameplay path can reach or free: nil slots, keys outside the +// 0..characterInventorySlots-1 range, and non-positive quantities. FreeSlots +// already ignores invalid entries, but FirstFreeSlot and the on-disk save still +// need the map itself to be clean, so sanitize here as well. +func sanitizeInventory(slots map[int]*InventorySlot) { + for i, slot := range slots { + if slot == nil || slot.Quantity <= 0 || i < 0 || i >= characterInventorySlots { + delete(slots, i) + } + } +} + +// sanitizeBank applies the same rule to the bank, which unlike the inventory +// grows past 28 slots (NextBankSlot), so only negative keys are out of range. +func sanitizeBank(slots map[int]*InventorySlot) { + for i, slot := range slots { + if slot == nil || slot.Quantity <= 0 || i < 0 { + delete(slots, i) + } + } +} + func (s *AccountStore) SaveCharacter(p *Player) error { data, err := s.MarshalCharacter(p) if err != nil { return err } - return persistence.WriteFile(s.CharPath(p.Name), data, 0600) + return writePrivate(s.CharPath(p.Name), data) } // MarshalCharacter serializes a character for a same-directory temporary |
