From 6696d27ee004eb54d050674e7256b1c1dfb60d1c Mon Sep 17 00:00:00 2001 From: Neale Pickett Date: Fri, 4 Sep 2020 18:28:23 -0600 Subject: [PATCH] Transpiler seems complete --- cmd/transpile/category.go | 2 + cmd/transpile/category_test.go | 1 + cmd/transpile/main.go | 49 +++------- cmd/transpile/main_test.go | 21 +++- cmd/transpile/mothball.go | 75 ++++++++++++++ cmd/transpile/mothball_test.go | 36 +++++++ cmd/transpile/puzzle.go | 172 ++++++++++++++++++++++++++++----- cmd/transpile/puzzle_test.go | 7 -- 8 files changed, 297 insertions(+), 66 deletions(-) create mode 100644 cmd/transpile/mothball_test.go diff --git a/cmd/transpile/category.go b/cmd/transpile/category.go index 9e8ebda..90e2ecd 100644 --- a/cmd/transpile/category.go +++ b/cmd/transpile/category.go @@ -143,6 +143,8 @@ func (c FsCommandCategory) Puzzle(points int) (Puzzle, error) { return p, err } + p.computeAnswerHashes() + return p, nil } diff --git a/cmd/transpile/category_test.go b/cmd/transpile/category_test.go index 025e4d5..2baea5f 100644 --- a/cmd/transpile/category_test.go +++ b/cmd/transpile/category_test.go @@ -32,6 +32,7 @@ func TestFsCategory(t *testing.T) { } if r, err := c.Open(1, "moo.txt"); err != nil { + t.Log(c.Puzzle(1)) t.Error(err) } else { defer r.Close() diff --git a/cmd/transpile/main.go b/cmd/transpile/main.go index ef46a61..dc36c96 100644 --- a/cmd/transpile/main.go +++ b/cmd/transpile/main.go @@ -28,39 +28,6 @@ type Category interface { Answer(points int, answer string) bool } -// Puzzle contains everything about a puzzle. -type Puzzle struct { - Pre struct { - Authors []string - Attachments []Attachment - Scripts []Attachment - AnswerPattern string - Body string - } - Post struct { - Objective string - Success struct { - Acceptable string - Mastery string - } - KSAs []string - } - Debug struct { - Log []string - Errors []string - Hints []string - Summary string - } - Answers []string -} - -// Attachment carries information about an attached file. -type Attachment struct { - Filename string // Filename presented as part of puzzle - FilesystemPath string // Filename in backing FS (URL, mothball, or local FS) - Listed bool // Whether this file is listed as an attachment -} - // T contains everything required for a transpilation invocation (across the nation). type T struct { // What action to take @@ -95,6 +62,8 @@ func (t *T) Handle(action string) error { return t.PrintInventory() case "open": return t.Open() + case "mothball": + return t.Mothball() default: return fmt.Errorf("Unimplemented action: %s", action) } @@ -134,6 +103,7 @@ func (t *T) Open() error { switch t.Filename { case "puzzle.json", "": + // BUG(neale): we need a way to tell the transpiler to strip answers p, err := c.Puzzle(t.Points) if err != nil { return err @@ -157,6 +127,19 @@ func (t *T) Open() error { return nil } +// Mothball writes a mothball to the writer. +func (t *T) Mothball() error { + c := t.NewCategory(t.Cat) + mb, err := Mothball(c) + if err != nil { + return err + } + if _, err := io.Copy(t.w, mb); err != nil { + return err + } + return nil +} + // NewCategory returns a new Fs-backed category. func (t *T) NewCategory(name string) Category { return NewFsCategory(t.Fs, name) diff --git a/cmd/transpile/main_test.go b/cmd/transpile/main_test.go index 76a0f8a..4dd9c8e 100644 --- a/cmd/transpile/main_test.go +++ b/cmd/transpile/main_test.go @@ -17,6 +17,8 @@ pre: - Arthur - Buster - DW + attachments: + - filename: moo.txt --- YAML body `) @@ -46,9 +48,12 @@ body `), 0644) afero.WriteFile(fs, "cat0/20/puzzle.md", []byte("Answer: no\nBadField: yes\n\nbody\n"), 0644) afero.WriteFile(fs, "cat0/21/puzzle.md", []byte("Answer: broken\nSpooon\n"), 0644) - afero.WriteFile(fs, "cat0/22/puzzle.md", []byte("---\nanswers:\n - pencil\npre:\n body: Spooon\n---\nSpoon?\n"), 0644) + afero.WriteFile(fs, "cat0/22/puzzle.md", []byte("---\nanswers:\n - pencil\npre:\n unused-field: Spooon\n---\nSpoon?\n"), 0644) afero.WriteFile(fs, "cat1/93/puzzle.md", []byte("Answer: no\n\nbody"), 0644) afero.WriteFile(fs, "cat1/barney/puzzle.md", testMothYaml, 0644) + afero.WriteFile(fs, "unbroken/1/puzzle.md", testMothYaml, 0644) + afero.WriteFile(fs, "unbroken/1/moo.txt", []byte("Moo."), 0644) + afero.WriteFile(fs, "unbroken/2/puzzle.md", testMothRfc822, 0644) return fs } @@ -62,7 +67,7 @@ func TestEverything(t *testing.T) { if err := tp.Handle("inventory"); err != nil { t.Error(err) } - if strings.TrimSpace(stdout.String()) != `{"cat0":[1,2,3,4,5,10,20,21,22],"cat1":[93]}` { + if strings.TrimSpace(stdout.String()) != `{"cat0":[1,2,3,4,5,10,20,21,22],"cat1":[93],"unbroken":[1,2]}` { t.Errorf("Bad inventory: %#v", stdout.String()) } @@ -89,4 +94,16 @@ func TestEverything(t *testing.T) { if stdout.String() != "Moo." { t.Error("Wrong file pulled") } + + stdout.Reset() + tp.Cat = "unbroken" + if err := tp.Handle("mothball"); err != nil { + t.Error(err) + } + if stdout.Len() < 200 { + t.Error("That's way too short to be a mothball") + } + if stdout.String()[:2] != "PK" { + t.Error("This mothball isn't a zip file!") + } } diff --git a/cmd/transpile/mothball.go b/cmd/transpile/mothball.go index c9ecbf5..2f95481 100644 --- a/cmd/transpile/mothball.go +++ b/cmd/transpile/mothball.go @@ -1,2 +1,77 @@ package main +import ( + "archive/zip" + "bytes" + "encoding/json" + "fmt" + "io" +) + +// Mothball packages a Category up for a production server run. +func Mothball(c Category) (*bytes.Reader, error) { + buf := new(bytes.Buffer) + zf := zip.NewWriter(buf) + + inv, err := c.Inventory() + if err != nil { + return nil, err + } + + puzzlesTxt, err := zf.Create("puzzles.txt") + if err != nil { + return nil, err + } + answersTxt, err := zf.Create("answers.txt") + if err != nil { + return nil, err + } + + for _, points := range inv { + fmt.Fprintln(puzzlesTxt, points) + + puzzlePath := fmt.Sprintf("%d/puzzle.json", points) + pw, err := zf.Create(puzzlePath) + if err != nil { + return nil, err + } + puzzle, err := c.Puzzle(points) + if err != nil { + return nil, err + } + + // Record answers in answers.txt + for _, answer := range puzzle.Answers { + fmt.Fprintln(answersTxt, points, answer) + } + + // Remove all answers from puzzle object + puzzle.Answers = []string{} + + // Write out Puzzle object + penc := json.NewEncoder(pw) + if err := penc.Encode(puzzle); err != nil { + return nil, err + } + + // Write out all attachments and scripts + attachments := append(puzzle.Pre.Attachments, puzzle.Pre.Scripts...) + for _, att := range attachments { + attPath := fmt.Sprintf("%d/%s", points, att) + aw, err := zf.Create(attPath) + if err != nil { + return nil, err + } + ar, err := c.Open(points, att) + if err != nil { + return nil, err + } + if _, err := io.Copy(aw, ar); err != nil { + return nil, err + } + } + } + zf.Close() + + return bytes.NewReader(buf.Bytes()), nil +} diff --git a/cmd/transpile/mothball_test.go b/cmd/transpile/mothball_test.go new file mode 100644 index 0000000..9b0391f --- /dev/null +++ b/cmd/transpile/mothball_test.go @@ -0,0 +1,36 @@ +package main + +import ( + "archive/zip" + "io/ioutil" + "testing" + + "github.com/spf13/afero" + "github.com/spf13/afero/zipfs" +) + +func TestMothballs(t *testing.T) { + fs := NewRecursiveBasePathFs(afero.NewOsFs(), "testdata") + static := NewFsCategory(fs, "static") + mb, err := Mothball(static) + if err != nil { + t.Error(err) + } + + mbr, err := zip.NewReader(mb, int64(mb.Len())) + if err != nil { + t.Error(err) + } + zfs := zipfs.New(mbr) + + if f, err := zfs.Open("puzzles.txt"); err != nil { + t.Error(err) + } else { + defer f.Close() + if buf, err := ioutil.ReadAll(f); err != nil { + t.Error(err) + } else if string(buf) != "" { + t.Error("Bad puzzles.txt", string(buf)) + } + } +} diff --git a/cmd/transpile/puzzle.go b/cmd/transpile/puzzle.go index ea88c25..dc5ee1d 100644 --- a/cmd/transpile/puzzle.go +++ b/cmd/transpile/puzzle.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "context" + "crypto/sha256" "encoding/json" "fmt" "io" @@ -20,6 +21,77 @@ import ( "gopkg.in/yaml.v2" ) +// Puzzle contains everything about a puzzle that a client would see. +type Puzzle struct { + Pre struct { + Authors []string + Attachments []string + Scripts []string + AnswerHashes []string + AnswerPattern string + Body string + } + Post struct { + Objective string + Success struct { + Acceptable string + Mastery string + } + KSAs []string + } + Debug struct { + Log []string + Errors []string + Hints []string + Summary string + } + Answers []string +} + +func (puzzle *Puzzle) computeAnswerHashes() { + if len(puzzle.Answers) == 0 { + return + } + puzzle.Pre.AnswerHashes = make([]string, len(puzzle.Answers)) + for i, answer := range puzzle.Answers { + sum := sha256.Sum256([]byte(answer)) + hexsum := fmt.Sprintf("%x", sum) + puzzle.Pre.AnswerHashes[i] = hexsum + } +} + +// StaticPuzzle contains everything a static puzzle might tell us. +type StaticPuzzle struct { + Pre struct { + Authors []string + Attachments []StaticAttachment + Scripts []StaticAttachment + AnswerPattern string + } + Post struct { + Objective string + Success struct { + Acceptable string + Mastery string + } + KSAs []string + } + Debug struct { + Log []string + Errors []string + Hints []string + Summary string + } + Answers []string +} + +// StaticAttachment carries information about an attached file. +type StaticAttachment struct { + Filename string // Filename presented as part of puzzle + FilesystemPath string // Filename in backing FS (URL, mothball, or local FS) + Listed bool // Whether this file is listed as an attachment +} + // PuzzleProvider establishes the functionality required to provide one puzzle. type PuzzleProvider interface { // Puzzle returns a Puzzle struct for the current puzzle. @@ -60,11 +132,64 @@ type FsPuzzle struct { // Puzzle returns a Puzzle struct for the current puzzle. func (fp FsPuzzle) Puzzle() (Puzzle, error) { + var puzzle Puzzle + + static, body, err := fp.staticPuzzle() + if err != nil { + return puzzle, err + } + + // Convert to an exportable Puzzle + puzzle.Post = static.Post + puzzle.Debug = static.Debug + puzzle.Answers = static.Answers + puzzle.Pre.Authors = static.Pre.Authors + puzzle.Pre.Body = string(body) + puzzle.Pre.AnswerPattern = static.Pre.AnswerPattern + puzzle.Pre.Attachments = make([]string, len(static.Pre.Attachments)) + for i, attachment := range static.Pre.Attachments { + puzzle.Pre.Attachments[i] = attachment.Filename + } + puzzle.Pre.Scripts = make([]string, len(static.Pre.Scripts)) + for i, script := range static.Pre.Scripts { + puzzle.Pre.Scripts[i] = script.Filename + } + puzzle.computeAnswerHashes() + + return puzzle, nil +} + +// Open returns a newly-opened file. +func (fp FsPuzzle) Open(name string) (io.ReadCloser, error) { + empty := ioutil.NopCloser(new(bytes.Buffer)) + static, _, err := fp.staticPuzzle() + if err != nil { + return empty, err + } + + var fsPath string + for _, attachment := range append(static.Pre.Attachments, static.Pre.Scripts...) { + if attachment.Filename == name { + if attachment.FilesystemPath == "" { + fsPath = attachment.Filename + } else { + fsPath = attachment.FilesystemPath + } + } + } + if fsPath == "" { + return empty, fmt.Errorf("Not listed in attachments or scripts: %s", name) + } + + return fp.fs.Open(fsPath) +} + +func (fp FsPuzzle) staticPuzzle() (StaticPuzzle, []byte, error) { r, err := fp.fs.Open("puzzle.md") if err != nil { var err2 error if r, err2 = fp.fs.Open("puzzle.moth"); err2 != nil { - return Puzzle{}, err + return StaticPuzzle{}, nil, err } } defer r.Close() @@ -101,33 +226,21 @@ func (fp FsPuzzle) Puzzle() (Puzzle, error) { bodyBuf.WriteRune('\n') } - puzzle, err := headerParser(headerBuf) + static, err := headerParser(headerBuf) if err != nil { - return puzzle, err + return static, nil, err } - // Markdownify the body - if puzzle.Pre.Body != "" { - if bodyBuf.Len() > 0 { - return puzzle, fmt.Errorf("Puzzle body present in header and in moth body") - } - } else { - puzzle.Pre.Body = string(blackfriday.Run(bodyBuf.Bytes())) - } + body := blackfriday.Run(bodyBuf.Bytes()) - return puzzle, nil + return static, body, err } -// Open returns a newly-opened file. -func (fp FsPuzzle) Open(name string) (io.ReadCloser, error) { - return fp.fs.Open(name) -} - -func legacyAttachmentParser(val []string) []Attachment { - ret := make([]Attachment, len(val)) +func legacyAttachmentParser(val []string) []StaticAttachment { + ret := make([]StaticAttachment, len(val)) for idx, txt := range val { parts := strings.SplitN(txt, " ", 3) - cur := Attachment{} + cur := StaticAttachment{} cur.FilesystemPath = parts[0] if len(parts) > 1 { cur.Filename = parts[1] @@ -144,16 +257,16 @@ func legacyAttachmentParser(val []string) []Attachment { return ret } -func yamlHeaderParser(r io.Reader) (Puzzle, error) { - p := Puzzle{} +func yamlHeaderParser(r io.Reader) (StaticPuzzle, error) { + p := StaticPuzzle{} decoder := yaml.NewDecoder(r) decoder.SetStrict(true) err := decoder.Decode(&p) return p, err } -func rfc822HeaderParser(r io.Reader) (Puzzle, error) { - p := Puzzle{} +func rfc822HeaderParser(r io.Reader) (StaticPuzzle, error) { + p := StaticPuzzle{} m, err := mail.ReadMessage(r) if err != nil { return p, fmt.Errorf("Parsing RFC822 headers: %v", err) @@ -188,6 +301,15 @@ func rfc822HeaderParser(r io.Reader) (Puzzle, error) { // Answer checks whether the given answer is correct. func (fp FsPuzzle) Answer(answer string) bool { + p, _, err := fp.staticPuzzle() + if err != nil { + return false + } + for _, ans := range p.Answers { + if ans == answer { + return true + } + } return false } @@ -216,6 +338,8 @@ func (fp FsCommandPuzzle) Puzzle() (Puzzle, error) { return Puzzle{}, err } + puzzle.computeAnswerHashes() + return puzzle, nil } diff --git a/cmd/transpile/puzzle_test.go b/cmd/transpile/puzzle_test.go index ca58662..122e174 100644 --- a/cmd/transpile/puzzle_test.go +++ b/cmd/transpile/puzzle_test.go @@ -3,7 +3,6 @@ package main import ( "bytes" "io" - "strings" "testing" "github.com/spf13/afero" @@ -64,12 +63,6 @@ func TestPuzzle(t *testing.T) { if _, err := NewFsPuzzle(catFs, 21).Puzzle(); err == nil { t.Error("Boken RFC822 header") } - if p, err := NewFsPuzzle(catFs, 22).Puzzle(); err == nil { - t.Error("Duplicate bodies") - } else if !strings.HasPrefix(err.Error(), "Puzzle body present") { - t.Log(p) - t.Error("Wrong error for duplicate body:", err) - } { fs := afero.NewMemMapFs()