work on getting mkpuzzle to work

This commit is contained in:
Neale Pickett 2020-09-01 20:12:57 -06:00
parent 7c22520b70
commit 7b06171839
7 changed files with 273 additions and 151 deletions

64
cmd/transpile/basepath.go Normal file
View File

@ -0,0 +1,64 @@
package main
import (
"os"
"path/filepath"
"runtime"
"strings"
"github.com/spf13/afero"
)
// BasePathFs is an overloaded afero.BasePathFs that has a recursive RealPath().
type BasePathFs struct {
afero.Fs
source afero.Fs
path string
}
// NewBasePathFs returns a new BasePathFs.
func NewBasePathFs(source afero.Fs, path string) afero.Fs {
return &BasePathFs{
Fs: afero.NewBasePathFs(source, path),
source: source,
path: path,
}
}
// RealPath returns the real path to a file, "breaking out" of the BasePathFs.
func (b *BasePathFs) RealPath(name string) (path string, err error) {
if err := validateBasePathName(name); err != nil {
return name, err
}
bpath := filepath.Clean(b.path)
path = filepath.Clean(filepath.Join(bpath, name))
if parentBasePathFs, ok := b.source.(*BasePathFs); ok {
return parentBasePathFs.RealPath(path)
} else if parentBasePathFs, ok := b.source.(*afero.BasePathFs); ok {
return parentBasePathFs.RealPath(path)
}
if !strings.HasPrefix(path, bpath) {
return name, os.ErrNotExist
}
return path, nil
}
func validateBasePathName(name string) error {
if runtime.GOOS != "windows" {
// Not much to do here;
// the virtual file paths all look absolute on *nix.
return nil
}
// On Windows a common mistake would be to provide an absolute OS path
// We could strip out the base part, but that would not be very portable.
if filepath.IsAbs(name) {
return os.ErrNotExist
}
return nil
}

View File

@ -10,7 +10,7 @@ import (
// NewCategory returns a new category for the given path in the given fs. // NewCategory returns a new category for the given path in the given fs.
func NewCategory(fs afero.Fs, cat string) Category { func NewCategory(fs afero.Fs, cat string) Category {
return Category{ return Category{
Fs: afero.NewBasePathFs(fs, cat), Fs: NewBasePathFs(fs, cat),
} }
} }
@ -41,7 +41,7 @@ func (c Category) Puzzles() ([]int, error) {
return puzzles, nil return puzzles, nil
} }
// Puzzle returns the Puzzle associated with points. // PuzzleDir returns the PuzzleDir associated with points.
func (c Category) Puzzle(points int) (*Puzzle, error) { func (c Category) PuzzleDir(points int) *PuzzleDir {
return NewPuzzle(c.Fs, points) return NewPuzzleDir(c.Fs, points)
} }

View File

@ -86,20 +86,21 @@ func (t *T) PrintInventory() error {
// Open writes a file to the writer. // Open writes a file to the writer.
func (t *T) Open() error { func (t *T) Open() error {
c := t.NewCategory(t.Cat) c := t.NewCategory(t.Cat)
p, err := c.Puzzle(t.Points) pd := c.PuzzleDir(t.Points)
if err != nil {
return err
}
switch t.Filename { switch t.Filename {
case "puzzle.json", "": case "puzzle.json", "":
p, err := pd.Export()
if err != nil {
return err
}
jp, err := json.Marshal(p) jp, err := json.Marshal(p)
if err != nil { if err != nil {
return err return err
} }
t.w.Write(jp) t.w.Write(jp)
default: default:
f, err := p.Open(t.Filename) f, err := pd.Open(t.Filename)
if err != nil { if err != nil {
return err return err
} }

View File

@ -47,10 +47,11 @@ body
afero.WriteFile(fs, "cat0/21/puzzle.md", []byte("Answer: broken\nSpooon\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 body: Spooon\n---\nSpoon?\n"), 0644)
afero.WriteFile(fs, "cat1/93/puzzle.md", []byte("Answer: no\n\nbody"), 0644) afero.WriteFile(fs, "cat1/93/puzzle.md", []byte("Answer: no\n\nbody"), 0644)
afero.WriteFile(fs, "cat1/barney/puzzle.md", testMothYaml, 0644)
return fs return fs
} }
func TestThings(t *testing.T) { func TestEverything(t *testing.T) {
stdout := new(bytes.Buffer) stdout := new(bytes.Buffer)
tp := T{ tp := T{
w: stdout, w: stdout,
@ -75,8 +76,8 @@ func TestThings(t *testing.T) {
if err := json.Unmarshal(stdout.Bytes(), &p); err != nil { if err := json.Unmarshal(stdout.Bytes(), &p); err != nil {
t.Error(err) t.Error(err)
} }
if p.Answers[0] != "YAML answer" { if (len(p.Answers) != 1) || (p.Answers[0] != "YAML answer") {
t.Error("Didn't return the right object") t.Error("Didn't return the right object", p)
} }
stdout.Reset() stdout.Reset()

View File

@ -7,7 +7,9 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"net/mail" "net/mail"
"os"
"os/exec" "os/exec"
"strconv" "strconv"
"strings" "strings"
@ -18,18 +20,159 @@ import (
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
// NewPuzzle returns a new Puzzle for points. // NewPuzzleDir returns a new PuzzleDir for points.
func NewPuzzle(fs afero.Fs, points int) (*Puzzle, error) { func NewPuzzleDir(fs afero.Fs, points int) *PuzzleDir {
p := &Puzzle{ pd := &PuzzleDir{
fs: afero.NewBasePathFs(fs, strconv.Itoa(points)), fs: NewBasePathFs(fs, strconv.Itoa(points)),
}
if err := p.parseMoth(); err != nil {
return p, err
} }
// BUG(neale): Doesn't yet handle "puzzle.py" or "mkpuzzle" // BUG(neale): Doesn't yet handle "puzzle.py" or "mkpuzzle"
return p, nil return pd
}
// PuzzleDir is a single puzzle's directory.
type PuzzleDir struct {
fs afero.Fs
mkpuzzle bool
}
// Open returns a newly-opened file.
func (pd *PuzzleDir) Open(name string) (io.ReadCloser, error) {
// BUG(neale): You cannot open generated files in puzzles, only files actually on the disk
if _, err := pd.fs.Stat(""
return pd.fs.Open(name)
}
// Export returns a Puzzle struct for the current puzzle.
func (pd *PuzzleDir) Export() (Puzzle, error) {
p, staticErr := pd.exportStatic()
if staticErr == nil {
return p, nil
}
// Only fall through if the static files don't exist. Otherwise, report the error.
if !os.IsNotExist(staticErr) {
return p, staticErr
}
if p, cmdErr := pd.exportCommand(); cmdErr == nil {
return p, nil
} else if os.IsNotExist(cmdErr) {
// If the command doesn't exist either, report the non-existence of the static file instead.
return p, staticErr
} else {
return p, cmdErr
}
}
func (pd *PuzzleDir) exportStatic() (Puzzle, error) {
r, err := pd.fs.Open("puzzle.md")
if err != nil {
var err2 error
if r, err2 = pd.fs.Open("puzzle.moth"); err2 != nil {
return Puzzle{}, err
}
}
defer r.Close()
headerBuf := new(bytes.Buffer)
headerParser := rfc822HeaderParser
headerEnd := ""
scanner := bufio.NewScanner(r)
lineNo := 0
for scanner.Scan() {
line := scanner.Text()
lineNo++
if lineNo == 1 {
if line == "---" {
headerParser = yamlHeaderParser
headerEnd = "---"
continue
}
}
if line == headerEnd {
headerBuf.WriteRune('\n')
break
}
headerBuf.WriteString(line)
headerBuf.WriteRune('\n')
}
bodyBuf := new(bytes.Buffer)
for scanner.Scan() {
line := scanner.Text()
lineNo++
bodyBuf.WriteString(line)
bodyBuf.WriteRune('\n')
}
puzzle, err := headerParser(headerBuf)
if err != nil {
return puzzle, 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()))
}
return puzzle, nil
}
func (pd *PuzzleDir) exportCommand() (Puzzle, error) {
bfs, ok := pd.fs.(*BasePathFs)
if !ok {
return Puzzle{}, fmt.Errorf("Fs won't resolve real paths for %v", pd)
}
mkpuzzlePath, err := bfs.RealPath("mkpuzzle")
if err != nil {
return Puzzle{}, err
}
log.Print(mkpuzzlePath)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, mkpuzzlePath)
stdout, err := cmd.Output()
if err != nil {
return Puzzle{}, err
}
jsdec := json.NewDecoder(bytes.NewReader(stdout))
jsdec.DisallowUnknownFields()
puzzle := Puzzle{}
if err := jsdec.Decode(&puzzle); err != nil {
return Puzzle{}, err
}
return puzzle, nil
}
func legacyAttachmentParser(val []string) []Attachment {
ret := make([]Attachment, len(val))
for idx, txt := range val {
parts := strings.SplitN(txt, " ", 3)
cur := Attachment{}
cur.FilesystemPath = parts[0]
if len(parts) > 1 {
cur.Filename = parts[1]
} else {
cur.Filename = cur.FilesystemPath
}
if (len(parts) > 2) && (parts[2] == "hidden") {
cur.Listed = false
} else {
cur.Listed = true
}
ret[idx] = cur
}
return ret
} }
// Puzzle contains everything about a puzzle. // Puzzle contains everything about a puzzle.
@ -56,7 +199,6 @@ type Puzzle struct {
Summary string Summary string
} }
Answers []string Answers []string
fs afero.Fs
} }
// Attachment carries information about an attached file. // Attachment carries information about an attached file.
@ -66,37 +208,19 @@ type Attachment struct {
Listed bool // Whether this file is listed as an attachment Listed bool // Whether this file is listed as an attachment
} }
func legacyAttachmentParser(val []string) []Attachment { func yamlHeaderParser(r io.Reader) (Puzzle, error) {
ret := make([]Attachment, len(val)) p := Puzzle{}
for idx, txt := range val {
parts := strings.SplitN(txt, " ", 3)
cur := Attachment{}
cur.FilesystemPath = parts[0]
if len(parts) > 1 {
cur.Filename = parts[1]
} else {
cur.Filename = cur.FilesystemPath
}
if (len(parts) > 2) && (parts[2] == "hidden") {
cur.Listed = false
} else {
cur.Listed = true
}
ret[idx] = cur
}
return ret
}
func (p *Puzzle) yamlHeaderParser(r io.Reader) error {
decoder := yaml.NewDecoder(r) decoder := yaml.NewDecoder(r)
decoder.SetStrict(true) decoder.SetStrict(true)
return decoder.Decode(p) err := decoder.Decode(&p)
return p, err
} }
func (p *Puzzle) rfc822HeaderParser(r io.Reader) error { func rfc822HeaderParser(r io.Reader) (Puzzle, error) {
p := Puzzle{}
m, err := mail.ReadMessage(r) m, err := mail.ReadMessage(r)
if err != nil { if err != nil {
return fmt.Errorf("Parsing RFC822 headers: %v", err) return p, fmt.Errorf("Parsing RFC822 headers: %v", err)
} }
for key, val := range m.Header { for key, val := range m.Header {
@ -119,99 +243,9 @@ func (p *Puzzle) rfc822HeaderParser(r io.Reader) error {
case "ksa": case "ksa":
p.Post.KSAs = val p.Post.KSAs = val
default: default:
return fmt.Errorf("Unknown header field: %s", key) return p, fmt.Errorf("Unknown header field: %s", key)
} }
} }
return nil return p, nil
}
func (p *Puzzle) parseMoth() error {
r, err := p.fs.Open("puzzle.md")
if err != nil {
var err2 error
if r, err2 = p.fs.Open("puzzle.moth"); err2 != nil {
return err
}
}
defer r.Close()
headerBuf := new(bytes.Buffer)
headerParser := p.rfc822HeaderParser
headerEnd := ""
scanner := bufio.NewScanner(r)
lineNo := 0
for scanner.Scan() {
line := scanner.Text()
lineNo++
if lineNo == 1 {
if line == "---" {
headerParser = p.yamlHeaderParser
headerEnd = "---"
continue
}
}
if line == headerEnd {
headerBuf.WriteRune('\n')
break
}
headerBuf.WriteString(line)
headerBuf.WriteRune('\n')
}
bodyBuf := new(bytes.Buffer)
for scanner.Scan() {
line := scanner.Text()
lineNo++
bodyBuf.WriteString(line)
bodyBuf.WriteRune('\n')
}
if err := headerParser(headerBuf); err != nil {
return err
}
// Markdownify the body
if (p.Pre.Body != "") && (bodyBuf.Len() > 0) {
return fmt.Errorf("Puzzle body present in header and in moth body")
}
p.Pre.Body = string(blackfriday.Run(bodyBuf.Bytes()))
return nil
}
func (p *Puzzle) mkpuzzle() error {
bfs, ok := p.fs.(*afero.BasePathFs)
if !ok {
return fmt.Errorf("Fs won't resolve real paths for %v", p)
}
mkpuzzlePath, err := bfs.RealPath("mkpuzzle")
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, mkpuzzlePath)
stdout, err := cmd.Output()
if err != nil {
return err
}
jsdec := json.NewDecoder(bytes.NewReader(stdout))
jsdec.DisallowUnknownFields()
puzzle := new(Puzzle)
if err := jsdec.Decode(puzzle); err != nil {
return err
}
return nil
}
// Open returns a newly-opened file.
func (p *Puzzle) Open(name string) (io.ReadCloser, error) {
// BUG(neale): You cannot open generated files in puzzles, only files actually on the disk
return p.fs.Open(name)
} }

View File

@ -1,6 +1,8 @@
package main package main
import ( import (
"bytes"
"io"
"strings" "strings"
"testing" "testing"
@ -12,11 +14,12 @@ func TestPuzzle(t *testing.T) {
catFs := afero.NewBasePathFs(puzzleFs, "cat0") catFs := afero.NewBasePathFs(puzzleFs, "cat0")
{ {
p, err := NewPuzzle(catFs, 1) pd := NewPuzzleDir(catFs, 1)
p, err := pd.Export()
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
t.Log(p)
if (len(p.Answers) == 0) || (p.Answers[0] != "YAML answer") { if (len(p.Answers) == 0) || (p.Answers[0] != "YAML answer") {
t.Error("Answers are wrong", p.Answers) t.Error("Answers are wrong", p.Answers)
} }
@ -29,7 +32,7 @@ func TestPuzzle(t *testing.T) {
} }
{ {
p, err := NewPuzzle(catFs, 2) p, err := NewPuzzleDir(catFs, 2).Export()
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
@ -44,26 +47,27 @@ func TestPuzzle(t *testing.T) {
} }
} }
if _, err := NewPuzzle(catFs, 3); err != nil { if _, err := NewPuzzleDir(catFs, 3).Export(); err != nil {
t.Error("Legacy `puzzle.moth` file:", err) t.Error("Legacy `puzzle.moth` file:", err)
} }
if _, err := NewPuzzle(catFs, 99); err == nil { if _, err := NewPuzzleDir(catFs, 99).Export(); err == nil {
t.Error("Non-existent puzzle", err) t.Error("Non-existent puzzle", err)
} }
if _, err := NewPuzzle(catFs, 10); err == nil { if _, err := NewPuzzleDir(catFs, 10).Export(); err == nil {
t.Error("Broken YAML") t.Error("Broken YAML")
} }
if _, err := NewPuzzle(catFs, 20); err == nil { if _, err := NewPuzzleDir(catFs, 20).Export(); err == nil {
t.Error("Bad RFC822 header") t.Error("Bad RFC822 header")
} }
if _, err := NewPuzzle(catFs, 21); err == nil { if _, err := NewPuzzleDir(catFs, 21).Export(); err == nil {
t.Error("Boken RFC822 header") t.Error("Boken RFC822 header")
} }
if _, err := NewPuzzle(catFs, 22); err == nil { if p, err := NewPuzzleDir(catFs, 22).Export(); err == nil {
t.Error("Duplicate bodies") t.Error("Duplicate bodies")
} else if !strings.HasPrefix(err.Error(), "Puzzle body present") { } else if !strings.HasPrefix(err.Error(), "Puzzle body present") {
t.Log(p)
t.Error("Wrong error for duplicate body:", err) t.Error("Wrong error for duplicate body:", err)
} }
} }
@ -71,11 +75,29 @@ func TestPuzzle(t *testing.T) {
func TestFsPuzzle(t *testing.T) { func TestFsPuzzle(t *testing.T) {
catFs := afero.NewBasePathFs(afero.NewOsFs(), "testdata") catFs := afero.NewBasePathFs(afero.NewOsFs(), "testdata")
if _, err := NewPuzzle(catFs, 1); err != nil { if _, err := NewPuzzleDir(catFs, 1).Export(); err != nil {
t.Error(err) t.Error(err)
} }
if _, err := NewPuzzle(catFs, 2); err != nil { if _, err := NewPuzzleDir(catFs, 2).Export(); err != nil {
t.Error(err) t.Error(err)
} }
mkpuzzleDir := NewPuzzleDir(catFs, 3)
if _, err := mkpuzzleDir.Export(); err != nil {
t.Error(err)
}
if body, err := mkpuzzleDir.Open("moo.txt"); err != nil {
t.Error(err)
} else {
defer body.Close()
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, body); err != nil {
t.Error(err)
}
if buf.String() != "Moo.\n" {
t.Error("Wrong body")
}
}
} }

View File

@ -6,7 +6,7 @@ case $1 in
{ {
"Answers": ["answer"], "Answers": ["answer"],
"Pre": { "Pre": {
"Authors": "neale", "Authors": ["neale"],
"Body": "I am a generated puzzle." "Body": "I am a generated puzzle."
} }
} }