|
- // Copyright (c) 2016 Marty Schoch
-
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the
- // License. You may obtain a copy of the License at
- // http://www.apache.org/licenses/LICENSE-2.0
- // Unless required by applicable law or agreed to in writing,
- // software distributed under the License is distributed on an "AS
- // IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- // express or implied. See the License for the specific language
- // governing permissions and limitations under the License.
-
- package smat
-
- import (
- "bufio"
- "bytes"
- "fmt"
- "io"
- "io/ioutil"
- "log"
- "math/rand"
- )
-
- // Logger is a configurable logger used by this package
- // by default output is discarded
- var Logger = log.New(ioutil.Discard, "smat ", log.LstdFlags)
-
- // Context is a container for any user state
- type Context interface{}
-
- // State is a function which describes which action to perform in the event
- // that a particular byte is seen
- type State func(next byte) ActionID
-
- // PercentAction describes the frequency with which an action should occur
- // for example: Action{Percent:10, Action:DonateMoney} means that 10% of
- // the time you should donate money.
- type PercentAction struct {
- Percent int
- Action ActionID
- }
-
- // Action is any function which returns the next state to transition to
- // it can optionally mutate the provided context object
- // if any error occurs, it may return an error which will abort execution
- type Action func(Context) (State, error)
-
- // ActionID is a unique identifier for an action
- type ActionID int
-
- // NopAction does nothing and simply continues to the next input
- var NopAction ActionID = -1
-
- // ActionMap is a mapping form ActionID to Action
- type ActionMap map[ActionID]Action
-
- func (a ActionMap) findSetupTeardown(setup, teardown ActionID) (Action, Action, error) {
- setupFunc, ok := a[setup]
- if !ok {
- return nil, nil, ErrSetupMissing
- }
- teardownFunc, ok := a[teardown]
- if !ok {
- return nil, nil, ErrTeardownMissing
- }
- return setupFunc, teardownFunc, nil
- }
-
- // Fuzz runs the fuzzing state machine with the provided context
- // first, the setup action is executed unconditionally
- // the start state is determined by this action
- // actionMap is a lookup table for all actions
- // the data byte slice determines all future state transitions
- // finally, the teardown action is executed unconditionally for cleanup
- func Fuzz(ctx Context, setup, teardown ActionID, actionMap ActionMap, data []byte) int {
- reader := bytes.NewReader(data)
- err := runReader(ctx, setup, teardown, actionMap, reader, nil)
- if err != nil {
- panic(err)
- }
- return 1
- }
-
- // Longevity runs the state machine with the provided context
- // first, the setup action is executed unconditionally
- // the start state is determined by this action
- // actionMap is a lookup table for all actions
- // random bytes are generated to determine all future state transitions
- // finally, the teardown action is executed unconditionally for cleanup
- func Longevity(ctx Context, setup, teardown ActionID, actionMap ActionMap, seed int64, closeChan chan struct{}) error {
- source := rand.NewSource(seed)
- return runReader(ctx, setup, teardown, actionMap, rand.New(source), closeChan)
- }
-
- var (
- // ErrSetupMissing is returned when the setup action cannot be found
- ErrSetupMissing = fmt.Errorf("setup action missing")
- // ErrTeardownMissing is returned when the teardown action cannot be found
- ErrTeardownMissing = fmt.Errorf("teardown action missing")
- // ErrClosed is returned when the closeChan was closed to cancel the op
- ErrClosed = fmt.Errorf("closed")
- // ErrActionNotPossible is returned when an action is encountered in a
- // FuzzCase that is not possible in the current state
- ErrActionNotPossible = fmt.Errorf("action not possible in state")
- )
-
- func runReader(ctx Context, setup, teardown ActionID, actionMap ActionMap, r io.Reader, closeChan chan struct{}) error {
- setupFunc, teardownFunc, err := actionMap.findSetupTeardown(setup, teardown)
- if err != nil {
- return err
- }
- Logger.Printf("invoking setup action")
- state, err := setupFunc(ctx)
- if err != nil {
- return err
- }
- defer func() {
- Logger.Printf("invoking teardown action")
- _, _ = teardownFunc(ctx)
- }()
-
- reader := bufio.NewReader(r)
- for next, err := reader.ReadByte(); err == nil; next, err = reader.ReadByte() {
- select {
- case <-closeChan:
- return ErrClosed
- default:
- actionID := state(next)
- action, ok := actionMap[actionID]
- if !ok {
- Logger.Printf("no such action defined, continuing")
- continue
- }
- Logger.Printf("invoking action - %d", actionID)
- state, err = action(ctx)
- if err != nil {
- Logger.Printf("it was action %d that returned err %v", actionID, err)
- return err
- }
- }
- }
- return err
- }
-
- // PercentExecute interprets the next byte as a random value and normalizes it
- // to values 0-99, it then looks to see which action should be execued based
- // on the action distributions
- func PercentExecute(next byte, pas ...PercentAction) ActionID {
- percent := int(99 * int(next) / 255)
-
- sofar := 0
- for _, pa := range pas {
- sofar = sofar + pa.Percent
- if percent < sofar {
- return pa.Action
- }
-
- }
- return NopAction
- }
|