aboutsummaryrefslogtreecommitdiff
path: root/internal/player/store.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/player/store.go')
-rw-r--r--internal/player/store.go44
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