diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2015-10-12 10:16:57 +0200 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2015-10-12 10:16:57 +0200 |
| commit | 874c5754bb22dbf77d6b600ff91f0f4f1fc5073a (patch) | |
| tree | 0075fbd088046ad5c86e6e972235701d68b3ce7c /manager | |
initial commit
Diffstat (limited to 'manager')
| -rw-r--r-- | manager/cover.go | 286 | ||||
| -rw-r--r-- | manager/example.cfg | 25 | ||||
| -rw-r--r-- | manager/html.go | 188 | ||||
| -rw-r--r-- | manager/main.go | 139 | ||||
| -rw-r--r-- | manager/manager.go | 238 |
5 files changed, 876 insertions, 0 deletions
diff --git a/manager/cover.go b/manager/cover.go new file mode 100644 index 000000000..accc230f9 --- /dev/null +++ b/manager/cover.go @@ -0,0 +1,286 @@ +// Copyright 2015 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package main + +import ( + "bufio" + "bytes" + "fmt" + "html/template" + "io" + "io/ioutil" + "os/exec" + "sort" + "strconv" + "strings" + "sync" +) + +type LineInfo struct { + file string + line int +} + +var ( + mu sync.Mutex + pcLines = make(map[uint32][]LineInfo) + parsedFiles = make(map[string][][]byte) + htmlReplacer = strings.NewReplacer(">", ">", "<", "<", "&", "&", "\t", " ") + sourcePrefix string +) + +func generateCoverHtml(w io.Writer, vmlinux string, cov []uint32) error { + mu.Lock() + defer mu.Unlock() + + info, err := covToLineInfo(vmlinux, cov) + if err != nil { + return err + } + files := fileSet(info) + for f := range files { + if _, ok := parsedFiles[f]; ok { + continue + } + if err := parseFile(f); err != nil { + return err + } + } + + var d templateData + for f, covered := range files { + lines := parsedFiles[f] + coverage := len(covered) + var buf bytes.Buffer + for i, ln := range lines { + if len(covered) > 0 && covered[0] == i+1 { + buf.Write([]byte("<span id='covered'>")) + buf.Write(ln) + buf.Write([]byte("</span>\n")) + covered = covered[1:] + } else { + buf.Write(ln) + buf.Write([]byte("\n")) + } + } + stripped := f + if len(stripped) > len(sourcePrefix) { + stripped = stripped[len(sourcePrefix):] + } + d.Files = append(d.Files, &templateFile{ + Name: stripped, + Body: template.HTML(buf.String()), + Coverage: coverage, + }) + } + + sort.Sort(templateFileArray(d.Files)) + if err := coverTemplate.Execute(w, d); err != nil { + return err + } + return nil +} + +func covToLineInfo(vmlinux string, cov []uint32) ([]LineInfo, error) { + var missing []uint32 + for _, pc := range cov { + if _, ok := pcLines[pc]; !ok { + missing = append(missing, pc) + } + } + if len(missing) > 0 { + if err := symbolize(vmlinux, missing); err != nil { + return nil, err + } + } + var info []LineInfo + for _, pc := range cov { + info = append(info, pcLines[pc]...) + } + return info, nil +} + +func fileSet(info []LineInfo) map[string][]int { + files := make(map[string]map[int]struct{}) + for _, li := range info { + if files[li.file] == nil { + files[li.file] = make(map[int]struct{}) + } + files[li.file][li.line] = struct{}{} + } + res := make(map[string][]int) + for f, lines := range files { + sorted := make([]int, 0, len(lines)) + for ln := range lines { + sorted = append(sorted, ln) + } + sort.Ints(sorted) + res[f] = sorted + } + return res +} + +func parseFile(fn string) error { + data, err := ioutil.ReadFile(fn) + if err != nil { + return err + } + var lines [][]byte + for { + idx := bytes.IndexByte(data, '\n') + if idx == -1 { + break + } + lines = append(lines, []byte(htmlReplacer.Replace(string(data[:idx])))) + data = data[idx+1:] + } + if len(data) != 0 { + lines = append(lines, data) + } + parsedFiles[fn] = lines + if sourcePrefix == "" { + sourcePrefix = fn + } else { + i := 0 + for ; i < len(sourcePrefix) && i < len(fn); i++ { + if sourcePrefix[i] != fn[i] { + break + } + } + sourcePrefix = sourcePrefix[:i] + } + return nil +} + +func symbolize(vmlinux string, cov []uint32) error { + cmd := exec.Command("addr2line", "-a", "-i", "-e", vmlinux) + stdin, err := cmd.StdinPipe() + if err != nil { + return err + } + defer stdin.Close() + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + defer stdout.Close() + if err := cmd.Start(); err != nil { + return err + } + defer cmd.Wait() + go func() { + for _, pc := range cov { + fmt.Fprintf(stdin, "0xffffffff%x\n", pc-1) + } + stdin.Close() + }() + s := bufio.NewScanner(stdout) + var pc uint32 + for s.Scan() { + ln := s.Text() + if len(ln) > 3 && ln[0] == '0' && ln[1] == 'x' { + v, err := strconv.ParseUint(ln, 0, 64) + if err != nil { + return fmt.Errorf("failed to parse pc in addr2line output: %v", err) + } + pc = uint32(v) + 1 + continue + } + colon := strings.IndexByte(ln, ':') + if colon == -1 { + continue + } + file := ln[:colon] + line, err := strconv.Atoi(ln[colon+1:]) + if err != nil || pc == 0 || file == "" || file == "??" || line <= 0 { + continue + } + pcLines[pc] = append(pcLines[pc], LineInfo{file, line}) + } + if err := s.Err(); err != nil { + return err + } + return nil +} + +type templateData struct { + Files []*templateFile +} + +type templateFile struct { + Name string + Body template.HTML + Coverage int +} + +type templateFileArray []*templateFile + +func (a templateFileArray) Len() int { return len(a) } +func (a templateFileArray) Less(i, j int) bool { return a[i].Name < a[j].Name } +func (a templateFileArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +var coverTemplate = template.Must(template.New("").Parse( + ` +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + <style> + body { + background: white; + } + #topbar { + background: black; + position: fixed; + top: 0; left: 0; right: 0; + height: 42px; + border-bottom: 1px solid rgb(70, 70, 70); + } + #nav { + float: left; + margin-left: 10px; + margin-top: 10px; + } + #content { + font-family: 'Courier New', Courier, monospace; + color: rgb(70, 70, 70); + margin-top: 50px; + } + #covered { + color: rgb(0, 0, 0); + font-weight: bold; + } + </style> + </head> + <body> + <div id="topbar"> + <div id="nav"> + <select id="files"> + {{range $i, $f := .Files}} + <option value="file{{$i}}">{{$f.Name}} ({{$f.Coverage}})</option> + {{end}} + </select> + </div> + </div> + <div id="content"> + {{range $i, $f := .Files}} + <pre class="file" id="file{{$i}}" {{if $i}}style="display: none"{{end}}>{{$f.Body}}</pre> + {{end}} + </div> + </body> + <script> + (function() { + var files = document.getElementById('files'); + var visible = document.getElementById('file0'); + files.addEventListener('change', onChange, false); + function onChange() { + visible.style.display = 'none'; + visible = document.getElementById(files.value); + visible.style.display = 'block'; + window.scrollTo(0, 0); + } + })(); + </script> +</html> +`)) diff --git a/manager/example.cfg b/manager/example.cfg new file mode 100644 index 000000000..216e09eeb --- /dev/null +++ b/manager/example.cfg @@ -0,0 +1,25 @@ +{ + "name": "my-qemu-asan", + "http": "myhost.com:56741", + "master": "myhost.com:48342", + "workdir": "/syzkaller/manager/workdir", + "vmlinux": "/linux/vmlinux", + "type": "qemu", + "count": 16, + "port": 23504, + "params": { + "kernel": "/linux/arch/x86/boot/bzImage", + "image": "/linux_image/wheezy.img", + "sshkey": "/linux_image/ssh/id_rsa", + "fuzzer": "/syzkaller/fuzzer/fuzzer", + "executor": "/syzkaller/executor/executor", + "port": 23505, + "cpu": 2, + "mem": 2048 + }, + "disable_syscalls": [ + "keyctl", + "add_key", + "request_key" + ] +} diff --git a/manager/html.go b/manager/html.go new file mode 100644 index 000000000..242492e38 --- /dev/null +++ b/manager/html.go @@ -0,0 +1,188 @@ +// Copyright 2015 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package main + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "html/template" + "net/http" + "sort" + "strconv" + + "github.com/google/syzkaller/cover" + "github.com/google/syzkaller/prog" +) + +func (mgr *Manager) httpInfo(w http.ResponseWriter, r *http.Request) { + mgr.mu.Lock() + defer mgr.mu.Unlock() + + type CallCov struct { + count int + cov cover.Cover + } + calls := make(map[string]*CallCov) + for _, inp := range mgr.corpus { + if calls[inp.Call] == nil { + calls[inp.Call] = new(CallCov) + } + cc := calls[inp.Call] + cc.count++ + cc.cov = cover.Union(cc.cov, cover.Cover(inp.Cover)) + } + + data := &UIData{ + Name: mgr.cfg.Name, + MasterHttp: mgr.masterHttp, + MasterCorpusSize: len(mgr.masterCorpus), + CorpusSize: len(mgr.corpus), + } + + var cov cover.Cover + for c, cc := range calls { + cov = cover.Union(cov, cc.cov) + data.Calls = append(data.Calls, UICallType{c, cc.count, len(cc.cov)}) + } + sort.Sort(UICallTypeArray(data.Calls)) + data.CoverSize = len(cov) + + if err := htmlTemplate.Execute(w, data); err != nil { + http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError) + } +} + +func (mgr *Manager) httpCorpus(w http.ResponseWriter, r *http.Request) { + mgr.mu.Lock() + defer mgr.mu.Unlock() + + var data []UIInput + call := r.FormValue("call") + for i, inp := range mgr.corpus { + if call != inp.Call { + continue + } + p, err := prog.Deserialize(inp.Prog) + if err != nil { + http.Error(w, fmt.Sprintf("failed to deserialize program: %v", err), http.StatusInternalServerError) + } + data = append(data, UIInput{ + Short: p.String(), + Full: string(inp.Prog), + Cover: len(inp.Cover), + N: i, + }) + } + sort.Sort(UIInputArray(data)) + + if err := corpusTemplate.Execute(w, data); err != nil { + http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError) + } +} + +func (mgr *Manager) httpCover(w http.ResponseWriter, r *http.Request) { + mgr.mu.Lock() + defer mgr.mu.Unlock() + + var cov cover.Cover + call := r.FormValue("call") + if n, err := strconv.Atoi(call); err == nil && n < len(mgr.corpus) { + cov = mgr.corpus[n].Cover + } else { + for _, inp := range mgr.corpus { + if call == "" || call == inp.Call { + cov = cover.Union(cov, cover.Cover(inp.Cover)) + } + } + } + + if err := generateCoverHtml(w, mgr.cfg.Vmlinux, cov); err != nil { + http.Error(w, fmt.Sprintf("failed to generate coverage profile: %v", err), http.StatusInternalServerError) + } +} + +func (mgr *Manager) httpCurrentCorpus(w http.ResponseWriter, r *http.Request) { + mgr.mu.Lock() + defer mgr.mu.Unlock() + + mgr.minimizeCorpus() + var hashes []string + for _, inp := range mgr.corpus { + hash := hash(inp.Prog) + hashes = append(hashes, hex.EncodeToString(hash[:])) + } + data, err := json.Marshal(&hashes) + if err != nil { + http.Error(w, fmt.Sprintf("failed to marshal corpus: %v", err), http.StatusInternalServerError) + return + } + w.Write(data) +} + +type UIData struct { + Name string + MasterHttp string + MasterCorpusSize int + CorpusSize int + CoverSize int + Calls []UICallType +} + +type UICallType struct { + Name string + Inputs int + Cover int +} + +type UIInput struct { + Short string + Full string + Calls int + Cover int + N int +} + +type UICallTypeArray []UICallType + +func (a UICallTypeArray) Len() int { return len(a) } +func (a UICallTypeArray) Less(i, j int) bool { return a[i].Name < a[j].Name } +func (a UICallTypeArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +type UIInputArray []UIInput + +func (a UIInputArray) Len() int { return len(a) } +func (a UIInputArray) Less(i, j int) bool { return a[i].Cover > a[j].Cover } +func (a UIInputArray) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +var htmlTemplate = template.Must(template.New("").Parse(` +<!doctype html> +<html> +<head> + <title>syzkaller {{.Name}}</title> +</head> +<body> +Manager: {{.Name}} <a href='http://{{.MasterHttp}}'>[master]</a> <br> +Master corpus: {{.MasterCorpusSize}} <br> +Corpus: {{.CorpusSize}}<br> +<a href='/cover'>Cover: {{.CoverSize}}</a> <br> +<br> +{{range $c := $.Calls}} + {{$c.Name}} <a href='/corpus?call={{$c.Name}}'>inputs:{{$c.Inputs}}</a> <a href='/cover?call={{$c.Name}}'>cover:{{$c.Cover}}</a><br> +{{end}} +</body></html> +`)) + +var corpusTemplate = template.Must(template.New("").Parse(` +<!doctype html> +<html> +<head> + <title>syzkaller corpus</title> +</head> +<body> +{{range $c := $}} + <span title="{{$c.Full}}">{{$c.Short}}</span> <a href='/cover?call={{$c.N}}'>cover:{{$c.Cover}}</a> <br> +{{end}} +</body></html> +`)) diff --git a/manager/main.go b/manager/main.go new file mode 100644 index 000000000..f83338918 --- /dev/null +++ b/manager/main.go @@ -0,0 +1,139 @@ +// Copyright 2015 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package main + +import ( + "encoding/json" + "flag" + "io/ioutil" + "log" + + "github.com/google/syzkaller/sys" + "github.com/google/syzkaller/vm" + _ "github.com/google/syzkaller/vm/local" + _ "github.com/google/syzkaller/vm/qemu" +) + +var ( + flagConfig = flag.String("config", "", "configuration file") + flagV = flag.Int("v", 0, "verbosity") +) + +type Config struct { + Name string + Http string + Master string + Workdir string + Vmlinux string + Type string + Count int + Port int + Nocover bool + Params map[string]interface{} + Enable_Syscalls []string + Disable_Syscalls []string +} + +func main() { + flag.Parse() + cfg, syscalls := parseConfig() + var instances []vm.Instance + for i := 0; i < cfg.Count; i++ { + params, err := json.Marshal(cfg.Params) + if err != nil { + fatalf("failed to marshal config params: %v", err) + } + inst, err := vm.Create(cfg.Type, cfg.Workdir, syscalls, cfg.Port, i, params) + if err != nil { + fatalf("failed to create an instance: %v", err) + } + instances = append(instances, inst) + } + RunManager(cfg, syscalls, instances) +} + +func parseConfig() (*Config, map[int]bool) { + if *flagConfig == "" { + fatalf("supply config file name in -config flag") + } + data, err := ioutil.ReadFile(*flagConfig) + if err != nil { + fatalf("failed to read config file: %v", err) + } + cfg := new(Config) + if err := json.Unmarshal(data, cfg); err != nil { + fatalf("failed to parse config file: %v", err) + } + if cfg.Name == "" { + fatalf("config param name is empty") + } + if cfg.Http == "" { + fatalf("config param http is empty") + } + if cfg.Master == "" { + fatalf("config param master is empty") + } + if cfg.Workdir == "" { + fatalf("config param workdir is empty") + } + if cfg.Vmlinux == "" { + fatalf("config param vmlinux is empty") + } + if cfg.Type == "" { + fatalf("config param type is empty") + } + if cfg.Count <= 0 || cfg.Count > 1000 { + fatalf("invalid config param count: %v, want (1, 1000]", cfg.Count) + } + + var syscalls map[int]bool + if len(cfg.Enable_Syscalls) != 0 || len(cfg.Disable_Syscalls) != 0 { + syscalls = make(map[int]bool) + if len(cfg.Enable_Syscalls) != 0 { + for _, c := range cfg.Enable_Syscalls { + n := 0 + for _, call := range sys.Calls { + if call.CallName == c { + syscalls[call.ID] = true + n++ + } + } + if n == 0 { + fatalf("unknown enabled syscall: %v", c) + } + } + } else { + for _, call := range sys.Calls { + syscalls[call.ID] = true + } + } + for _, c := range cfg.Disable_Syscalls { + n := 0 + for _, call := range sys.Calls { + if call.CallName == c { + delete(syscalls, call.ID) + n++ + } + } + if n == 0 { + fatalf("unknown disabled syscall: %v", c) + } + } + // They will be generated anyway. + syscalls[sys.CallMap["mmap"].ID] = true + syscalls[sys.CallMap["clock_gettime"].ID] = true + } + + return cfg, syscalls +} + +func logf(v int, msg string, args ...interface{}) { + if *flagV >= v { + log.Printf(msg, args...) + } +} + +func fatalf(msg string, args ...interface{}) { + log.Fatalf(msg, args...) +} diff --git a/manager/manager.go b/manager/manager.go new file mode 100644 index 000000000..0c6841db0 --- /dev/null +++ b/manager/manager.go @@ -0,0 +1,238 @@ +// Copyright 2015 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package main + +import ( + "crypto/sha1" + "fmt" + "net" + "net/http" + "net/rpc" + "sync" + "time" + + "github.com/google/syzkaller/cover" + "github.com/google/syzkaller/prog" + . "github.com/google/syzkaller/rpctype" + "github.com/google/syzkaller/sys" + "github.com/google/syzkaller/vm" +) + +type Sig [sha1.Size]byte + +func hash(data []byte) Sig { + return Sig(sha1.Sum(data)) +} + +type Manager struct { + cfg *Config + master *rpc.Client + masterHttp string + instances []vm.Instance + + mu sync.Mutex + masterCorpus [][]byte // mirror of master corpus + masterHashes map[Sig]struct{} // hashes of master corpus + candidates [][]byte // new untriaged inputs from master + syscalls map[int]bool + + corpus []RpcInput + corpusCover []cover.Cover + + fuzzers map[string]*Fuzzer +} + +type Fuzzer struct { + name string + input int +} + +func RunManager(cfg *Config, syscalls map[int]bool, instances []vm.Instance) { + // Connect to master. + master, err := rpc.Dial("tcp", cfg.Master) + if err != nil { + fatalf("failed to dial mastger: %v", err) + } + a := &MasterConnectArgs{cfg.Name, cfg.Http} + r := &MasterConnectRes{} + if err := master.Call("Master.Connect", a, r); err != nil { + fatalf("failed to connect to master: %v", err) + } + logf(0, "connected to master at %v", cfg.Master) + + mgr := &Manager{ + cfg: cfg, + master: master, + masterHttp: r.Http, + instances: instances, + masterHashes: make(map[Sig]struct{}), + syscalls: syscalls, + corpusCover: make([]cover.Cover, sys.CallCount), + fuzzers: make(map[string]*Fuzzer), + } + + http.HandleFunc("/", mgr.httpInfo) + http.HandleFunc("/corpus", mgr.httpCorpus) + http.HandleFunc("/cover", mgr.httpCover) + http.HandleFunc("/current_corpus", mgr.httpCurrentCorpus) + go func() { + logf(0, "serving http on http://%v", cfg.Http) + panic(http.ListenAndServe(cfg.Http, nil)) + }() + + // Create RPC server for fuzzers. + rpcAddr := fmt.Sprintf("localhost:%v", cfg.Port) + ln, err := net.Listen("tcp", rpcAddr) + if err != nil { + fatalf("failed to listen on port %v: %v", cfg.Port, err) + } + s := rpc.NewServer() + s.Register(mgr) + go s.Accept(ln) + logf(0, "serving rpc on tcp://%v", rpcAddr) + + mgr.run() +} + +func (mgr *Manager) run() { + mgr.pollMaster() + for _, inst := range mgr.instances { + go inst.Run() + } + pollTicker := time.NewTicker(10 * time.Second).C + for { + select { + case <-pollTicker: + mgr.mu.Lock() + mgr.pollMaster() + mgr.mu.Unlock() + } + } +} + +func (mgr *Manager) pollMaster() { + for { + a := &MasterPollArgs{mgr.cfg.Name} + r := &MasterPollRes{} + if err := mgr.master.Call("Master.PollInputs", a, r); err != nil { + fatalf("failed to poll master: %v", err) + } + logf(3, "polling master, got %v inputs", len(r.Inputs)) + if len(r.Inputs) == 0 { + break + } + nextProg: + for _, prg := range r.Inputs { + p, err := prog.Deserialize(prg) + if err != nil { + logf(0, "failed to deserialize master program: %v", err) + continue + } + if mgr.syscalls != nil { + for _, c := range p.Calls { + if !mgr.syscalls[c.Meta.ID] { + continue nextProg + } + } + } + sig := hash(prg) + if _, ok := mgr.masterHashes[sig]; ok { + continue + } + mgr.masterHashes[sig] = struct{}{} + mgr.masterCorpus = append(mgr.masterCorpus, prg) + mgr.candidates = append(mgr.candidates, prg) + } + } +} + +func (mgr *Manager) minimizeCorpus() { + if len(mgr.corpus) == 0 { + return + } + // First, sort corpus per call. + type Call struct { + inputs []RpcInput + cov []cover.Cover + } + calls := make(map[string]Call) + for _, inp := range mgr.corpus { + c := calls[inp.Call] + c.inputs = append(c.inputs, inp) + c.cov = append(c.cov, inp.Cover) + calls[inp.Call] = c + } + // Now minimize and build new corpus. + var newCorpus []RpcInput + for _, c := range calls { + for _, idx := range cover.Minimize(c.cov) { + newCorpus = append(newCorpus, c.inputs[idx]) + } + } + logf(1, "minimized corpus: %v -> %v", len(mgr.corpus), len(newCorpus)) + mgr.corpus = newCorpus +} + +func (mgr *Manager) Connect(a *ManagerConnectArgs, r *ManagerConnectRes) error { + logf(1, "fuzzer %v connected", a.Name) + mgr.mu.Lock() + defer mgr.mu.Unlock() + + mgr.minimizeCorpus() + mgr.fuzzers[a.Name] = &Fuzzer{ + name: a.Name, + input: 0, + } + return nil +} + +func (mgr *Manager) NewInput(a *NewManagerInputArgs, r *int) error { + logf(2, "new input from fuzzer %v", a.Name) + mgr.mu.Lock() + defer mgr.mu.Unlock() + + call := sys.CallID[a.Call] + if len(cover.Difference(a.Cover, mgr.corpusCover[call])) == 0 { + return nil + } + mgr.corpusCover[call] = cover.Union(mgr.corpusCover[call], a.Cover) + mgr.corpus = append(mgr.corpus, a.RpcInput) + + sig := hash(a.Prog) + if _, ok := mgr.masterHashes[sig]; !ok { + mgr.masterHashes[sig] = struct{}{} + mgr.masterCorpus = append(mgr.masterCorpus, a.Prog) + + a1 := &NewMasterInputArgs{mgr.cfg.Name, a.Prog} + if err := mgr.master.Call("Master.NewInput", a1, nil); err != nil { + fatalf("call Master.NewInput failed: %v", err) + } + } + + return nil +} + +func (mgr *Manager) Poll(a *ManagerPollArgs, r *ManagerPollRes) error { + logf(2, "poll from %v", a.Name) + mgr.mu.Lock() + defer mgr.mu.Unlock() + + f := mgr.fuzzers[a.Name] + if f == nil { + fatalf("fuzzer %v is not connected", a.Name) + } + + for i := 0; i < 100 && f.input < len(mgr.corpus); i++ { + r.NewInputs = append(r.NewInputs, mgr.corpus[f.input]) + f.input++ + } + + for i := 0; i < 10 && len(mgr.candidates) > 0; i++ { + last := len(mgr.candidates) - 1 + r.Candidates = append(r.Candidates, mgr.candidates[last]) + mgr.candidates = mgr.candidates[:last] + } + + return nil +} |
