fix: bound wizard-created Which against its item table (closes #10)
createObj stored the raw 0-f nibble as Object.Which with no bounds check, so wizard mode -> C -> / -> f made a wand numbered 15 against a 14-entry table and panicked in fixStick; input below 'a' or '0' went negative and panicked the same way. C's create_obj() was equally unchecked, but its consumers were either switches (defined for any value) or static-array reads past the end (undefined, and survivable in practice). Since one game is now one process, the Go panic kills the game outright and leaves the terminal in raw mode. Reject at the two boundaries a bad Which can enter through. createObj now refuses an out-of-range choice with a message drawn from C's own type_name() vocabulary and adds nothing to the pack, a deliberate divergence recorded in a comment because C had no defined behavior here to be faithful to. Restore refuses a snapshot describing such an object (ErrSaveCorrupt) rather than loading a game that would explode later. Behind those, whichLimit/hasValidWhich back defensive guards at every dispatch the issue names: the quaffHandler/readHandler/zapHandler accessors return no handler instead of indexing (for wands that is exactly what non-MASTER C did, matching no case and still running o_charges--), the callIt lore lookups, identifyType, armorClass for the a_class[] reads, initWeapon against the missing init_dam[] row for WeaponFlame, fixStick's ws_type[] read, and inventoryName and objectWorth, hoisted so one check each covers the whole family of per-kind name and appraisal tables. No in-range input changes behavior and no guard consumes a random number: the rejection precedes every rnd() call. TestSeedCompatItemTables stays green untouched. New game/wizard_test.go covers the exact reproducer, a rejection sweep over every indexed kind including both negative-input forms, an acceptance sweep proving valid choices still build the right item, one no-panic test per guarded family, the fixStick crash site, the corrupt-save rejection, and a check that whichLimit still agrees with the table sizes. Each guard was confirmed load-bearing by reverting it and watching the test panic. TODO.md records the step; Next Step is deliberately left alone, since this arrived out of band via an issue.
This commit is contained in:
@@ -859,6 +859,66 @@ func newGameData() *gameData {
|
||||
}
|
||||
}
|
||||
|
||||
// The three effect-table lookups below are the guarded form of a raw
|
||||
// index into quaffHandlers/readHandlers/zapHandlers. An object whose
|
||||
// Which is out of range for its kind reports no handler rather than
|
||||
// panicking, which lands on the same do-nothing behavior C's switches
|
||||
// had when no case matched.
|
||||
|
||||
// quaffHandler returns the potion effect for obj, or nil when obj is not
|
||||
// a potion the table knows about.
|
||||
func (d *gameData) quaffHandler(obj *Object) func(g *RogueGame, trip bool) {
|
||||
if !obj.hasValidWhich() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return d.quaffHandlers[obj.PotionKind()]
|
||||
}
|
||||
|
||||
// readHandler returns the scroll effect for obj, or nil when obj is not
|
||||
// a scroll the table knows about.
|
||||
func (d *gameData) readHandler(obj *Object) func(g *RogueGame, obj *Object) {
|
||||
if !obj.hasValidWhich() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return d.readHandlers[obj.ScrollKind()]
|
||||
}
|
||||
|
||||
// zapHandler returns the wand effect for obj, or nil when obj is not a
|
||||
// wand the table knows about.
|
||||
func (d *gameData) zapHandler(obj *Object) func(g *RogueGame, obj *Object) bool {
|
||||
if !obj.hasValidWhich() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return d.zapHandlers[obj.WandKind()]
|
||||
}
|
||||
|
||||
// identifyType reads extern.c's identify-scroll to item-kind map,
|
||||
// returning KindNone for any scroll past the last identify scroll (the
|
||||
// table is shorter than the scroll table it is keyed by).
|
||||
func (d *gameData) identifyType(kind ScrollKind) ObjectKind {
|
||||
if kind < 0 || int(kind) >= len(d.idType) {
|
||||
return KindNone
|
||||
}
|
||||
|
||||
return d.idType[kind]
|
||||
}
|
||||
|
||||
// armorClass reads extern.c a_class[] for an armor object's Which,
|
||||
// returning 0 when Which is out of range. Only a malformed object can
|
||||
// hit that arm — createObj and Restore both reject one — but a bounds
|
||||
// check here is what keeps a bad index from panicking the process
|
||||
// instead of merely naming a suit of armor oddly.
|
||||
func (d *gameData) armorClass(which int) int {
|
||||
if which < 0 || which >= int(NumArmorTypes) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return d.aClass[which]
|
||||
}
|
||||
|
||||
// Version strings (vers.c). The encstr/statlist XOR keys are not ported:
|
||||
// the Go save format does not use them.
|
||||
const (
|
||||
|
||||
Reference in New Issue
Block a user