2020-09-08 17:49:02 -06:00
|
|
|
package transpile
|
2019-08-17 11:01:26 -06:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"bytes"
|
2020-08-28 17:41:17 -06:00
|
|
|
"context"
|
2023-09-15 12:34:31 -06:00
|
|
|
"crypto/sha1"
|
2020-08-28 17:41:17 -06:00
|
|
|
"encoding/json"
|
2020-09-11 17:33:43 -06:00
|
|
|
"errors"
|
2019-08-17 11:01:26 -06:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2020-09-01 20:12:57 -06:00
|
|
|
"log"
|
2019-08-17 11:01:26 -06:00
|
|
|
"net/mail"
|
2020-10-20 11:15:46 -06:00
|
|
|
"os"
|
2020-08-28 17:41:17 -06:00
|
|
|
"os/exec"
|
2020-09-11 13:03:19 -06:00
|
|
|
"path"
|
2020-08-28 17:41:17 -06:00
|
|
|
"strconv"
|
2019-08-17 11:01:26 -06:00
|
|
|
"strings"
|
2020-08-28 17:41:17 -06:00
|
|
|
"time"
|
2020-08-14 20:26:04 -06:00
|
|
|
|
2020-08-28 17:41:17 -06:00
|
|
|
"github.com/spf13/afero"
|
2020-08-14 20:26:04 -06:00
|
|
|
"gopkg.in/yaml.v2"
|
2019-08-17 11:01:26 -06:00
|
|
|
)
|
|
|
|
|
2020-10-16 14:18:44 -06:00
|
|
|
// AnswerResponse is handed back when we ask for an answer to be checked.
|
|
|
|
type AnswerResponse struct {
|
|
|
|
Correct bool
|
|
|
|
}
|
|
|
|
|
2021-07-30 14:21:08 -06:00
|
|
|
// PuzzleDebug is the full suite of debug fields in a puzzle
|
|
|
|
|
|
|
|
type PuzzleDebug struct {
|
|
|
|
Log []string
|
|
|
|
Errors []string
|
|
|
|
Hints []string
|
|
|
|
Notes string
|
|
|
|
Summary string
|
|
|
|
}
|
|
|
|
|
2023-09-07 17:29:21 -06:00
|
|
|
// Puzzle contains everything about a puzzle that a client will see.
|
2020-09-04 18:28:23 -06:00
|
|
|
type Puzzle struct {
|
2023-09-07 17:29:21 -06:00
|
|
|
// Debug contains debugging information, omitted in mothballs
|
|
|
|
Debug PuzzleDebug
|
|
|
|
|
|
|
|
// Authors names all authors of this puzzle
|
|
|
|
Authors []string
|
|
|
|
|
|
|
|
// Attachments is a list of filenames used by this puzzle
|
|
|
|
Attachments []string
|
|
|
|
|
|
|
|
// Scripts is a list of EMCAScript files needed by the client for this puzzle
|
|
|
|
Scripts []string
|
|
|
|
|
|
|
|
// Body is the HTML rendering of this puzzle
|
|
|
|
Body string
|
|
|
|
|
|
|
|
// AnswerPattern contains the pattern (regular expression?) used to match valid answers
|
2021-02-24 16:34:35 -07:00
|
|
|
AnswerPattern string
|
2023-09-07 17:29:21 -06:00
|
|
|
|
|
|
|
// AnswerHashes contains hashes of all answers for this puzzle
|
|
|
|
AnswerHashes []string
|
|
|
|
|
2024-01-03 14:28:50 -07:00
|
|
|
// Answers lists all acceptable answers, omitted in mothballs
|
|
|
|
Answers []string
|
|
|
|
|
|
|
|
// Extra is send unchanged to the client.
|
|
|
|
// Eventually, Objective, KSAs, and Success will move into Extra.
|
|
|
|
Extra map[string]any
|
|
|
|
|
2023-09-07 17:29:21 -06:00
|
|
|
// Objective is the learning objective for this puzzle
|
|
|
|
Objective string
|
|
|
|
|
|
|
|
// KSAs lists all KSAs achieved upon successfull completion of this puzzle
|
|
|
|
KSAs []string
|
|
|
|
|
|
|
|
// Success lists the criteria for successfully understanding this puzzle
|
|
|
|
Success struct {
|
|
|
|
// Acceptable describes the minimum work required to be considered successfully understanding this puzzle's concepts
|
2021-02-24 16:34:35 -07:00
|
|
|
Acceptable string
|
2023-09-07 17:29:21 -06:00
|
|
|
|
2024-04-11 16:44:01 -06:00
|
|
|
// Mastery describes the work required to be considered mastering this puzzle's concepts
|
2023-09-07 17:29:21 -06:00
|
|
|
Mastery string
|
2021-02-24 16:34:35 -07:00
|
|
|
}
|
2020-09-04 18:28:23 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func (puzzle *Puzzle) computeAnswerHashes() {
|
|
|
|
if len(puzzle.Answers) == 0 {
|
|
|
|
return
|
|
|
|
}
|
2021-02-24 16:34:35 -07:00
|
|
|
puzzle.AnswerHashes = make([]string, len(puzzle.Answers))
|
2020-09-04 18:28:23 -06:00
|
|
|
for i, answer := range puzzle.Answers {
|
2023-09-15 12:34:31 -06:00
|
|
|
sum := sha1.Sum([]byte(answer))
|
2020-09-04 18:28:23 -06:00
|
|
|
hexsum := fmt.Sprintf("%x", sum)
|
2023-09-15 12:34:31 -06:00
|
|
|
puzzle.AnswerHashes[i] = hexsum[:4]
|
2020-09-04 18:28:23 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// StaticPuzzle contains everything a static puzzle might tell us.
|
|
|
|
type StaticPuzzle struct {
|
2021-02-24 16:34:35 -07:00
|
|
|
Authors []string
|
|
|
|
Attachments []StaticAttachment
|
|
|
|
Scripts []StaticAttachment
|
|
|
|
AnswerPattern string
|
2024-01-03 15:56:20 -07:00
|
|
|
Answers []string
|
|
|
|
Debug PuzzleDebug
|
|
|
|
Extra map[string]any
|
2021-02-24 16:34:35 -07:00
|
|
|
Objective string
|
|
|
|
Success struct {
|
|
|
|
Acceptable string
|
|
|
|
Mastery string
|
2020-09-04 18:28:23 -06:00
|
|
|
}
|
2024-01-03 15:56:20 -07:00
|
|
|
KSAs []string
|
2020-09-04 18:28:23 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2020-09-15 15:58:21 -06:00
|
|
|
// UnmarshalYAML allows a StaticAttachment to be specified as a single string.
|
|
|
|
// The way the yaml library works is weird.
|
|
|
|
func (sa *StaticAttachment) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
|
|
if err := unmarshal(&sa.Filename); err == nil {
|
|
|
|
sa.FilesystemPath = sa.Filename
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
parts := new(struct {
|
|
|
|
Filename string
|
|
|
|
FilesystemPath string
|
|
|
|
})
|
|
|
|
if err := unmarshal(parts); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
sa.Filename = parts.Filename
|
|
|
|
sa.FilesystemPath = parts.FilesystemPath
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-09-08 17:49:02 -06:00
|
|
|
// ReadSeekCloser provides io.Reader, io.Seeker, and io.Closer.
|
|
|
|
type ReadSeekCloser interface {
|
|
|
|
io.Reader
|
|
|
|
io.Seeker
|
|
|
|
io.Closer
|
|
|
|
}
|
|
|
|
|
2020-09-04 13:00:23 -06:00
|
|
|
// PuzzleProvider establishes the functionality required to provide one puzzle.
|
|
|
|
type PuzzleProvider interface {
|
|
|
|
// Puzzle returns a Puzzle struct for the current puzzle.
|
|
|
|
Puzzle() (Puzzle, error)
|
|
|
|
|
|
|
|
// Open returns a newly-opened file.
|
2020-09-08 17:49:02 -06:00
|
|
|
Open(filename string) (ReadSeekCloser, error)
|
2020-09-04 13:00:23 -06:00
|
|
|
|
|
|
|
// Answer returns whether the provided answer is correct.
|
|
|
|
Answer(answer string) bool
|
|
|
|
}
|
|
|
|
|
2020-09-11 13:03:19 -06:00
|
|
|
// NewFsPuzzle returns a new FsPuzzle.
|
|
|
|
func NewFsPuzzle(fs afero.Fs) PuzzleProvider {
|
|
|
|
var command string
|
|
|
|
|
2021-03-26 18:20:49 -06:00
|
|
|
bfs := NewRecursiveBasePathFs(fs, "")
|
|
|
|
if info, err := bfs.Stat("mkpuzzle"); !os.IsNotExist(err) {
|
2020-10-20 11:15:46 -06:00
|
|
|
if (info.Mode() & 0100) != 0 {
|
2021-03-26 18:20:49 -06:00
|
|
|
if command, err = bfs.RealPath(info.Name()); err != nil {
|
|
|
|
log.Println("WARN: Unable to resolve full path to", info.Name())
|
2020-09-04 13:00:23 -06:00
|
|
|
}
|
2020-10-20 11:15:46 -06:00
|
|
|
} else {
|
|
|
|
log.Println("WARN: mkpuzzle exists, but isn't executable.")
|
2020-09-04 13:00:23 -06:00
|
|
|
}
|
2020-08-28 17:41:17 -06:00
|
|
|
}
|
|
|
|
|
2020-09-11 13:03:19 -06:00
|
|
|
if command != "" {
|
|
|
|
return FsCommandPuzzle{
|
|
|
|
fs: fs,
|
|
|
|
command: command,
|
|
|
|
timeout: 2 * time.Second,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-04 13:00:23 -06:00
|
|
|
return FsPuzzle{
|
2020-09-11 13:03:19 -06:00
|
|
|
fs: fs,
|
2020-09-04 13:00:23 -06:00
|
|
|
}
|
2020-09-11 13:03:19 -06:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewFsPuzzlePoints returns a new FsPuzzle for points.
|
|
|
|
func NewFsPuzzlePoints(fs afero.Fs, points int) PuzzleProvider {
|
|
|
|
return NewFsPuzzle(NewRecursiveBasePathFs(fs, strconv.Itoa(points)))
|
2019-08-17 13:09:09 -06:00
|
|
|
}
|
|
|
|
|
2020-09-03 20:04:43 -06:00
|
|
|
// FsPuzzle is a single puzzle's directory.
|
|
|
|
type FsPuzzle struct {
|
|
|
|
fs afero.Fs
|
2020-09-01 20:12:57 -06:00
|
|
|
mkpuzzle bool
|
2019-08-17 11:01:26 -06:00
|
|
|
}
|
|
|
|
|
2020-09-03 20:04:43 -06:00
|
|
|
// Puzzle returns a Puzzle struct for the current puzzle.
|
|
|
|
func (fp FsPuzzle) Puzzle() (Puzzle, error) {
|
2021-02-24 16:34:35 -07:00
|
|
|
puzzle := Puzzle{}
|
2020-09-04 18:28:23 -06:00
|
|
|
|
|
|
|
static, body, err := fp.staticPuzzle()
|
|
|
|
if err != nil {
|
|
|
|
return puzzle, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert to an exportable Puzzle
|
|
|
|
puzzle.Debug = static.Debug
|
|
|
|
puzzle.Answers = static.Answers
|
2021-02-24 16:34:35 -07:00
|
|
|
puzzle.Authors = static.Authors
|
2024-01-03 15:56:20 -07:00
|
|
|
puzzle.Extra = static.Extra
|
2021-02-25 13:07:44 -07:00
|
|
|
puzzle.Objective = static.Objective
|
2021-04-13 17:28:22 -06:00
|
|
|
puzzle.KSAs = static.KSAs
|
2021-02-25 13:07:44 -07:00
|
|
|
puzzle.Success = static.Success
|
2021-02-24 16:34:35 -07:00
|
|
|
puzzle.Body = string(body)
|
|
|
|
puzzle.AnswerPattern = static.AnswerPattern
|
|
|
|
puzzle.Attachments = make([]string, len(static.Attachments))
|
|
|
|
for i, attachment := range static.Attachments {
|
|
|
|
puzzle.Attachments[i] = attachment.Filename
|
2020-09-04 18:28:23 -06:00
|
|
|
}
|
2021-02-24 16:34:35 -07:00
|
|
|
puzzle.Scripts = make([]string, len(static.Scripts))
|
|
|
|
for i, script := range static.Scripts {
|
|
|
|
puzzle.Scripts[i] = script.Filename
|
2020-09-04 18:28:23 -06:00
|
|
|
}
|
|
|
|
puzzle.computeAnswerHashes()
|
|
|
|
|
|
|
|
return puzzle, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Open returns a newly-opened file.
|
2020-09-08 17:49:02 -06:00
|
|
|
func (fp FsPuzzle) Open(name string) (ReadSeekCloser, error) {
|
|
|
|
empty := nopCloser{new(bytes.Reader)}
|
2020-09-04 18:28:23 -06:00
|
|
|
static, _, err := fp.staticPuzzle()
|
|
|
|
if err != nil {
|
|
|
|
return empty, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var fsPath string
|
2021-02-24 16:34:35 -07:00
|
|
|
for _, attachment := range append(static.Attachments, static.Scripts...) {
|
2020-09-04 18:28:23 -06:00
|
|
|
if attachment.Filename == name {
|
|
|
|
if attachment.FilesystemPath == "" {
|
|
|
|
fsPath = attachment.Filename
|
|
|
|
} else {
|
|
|
|
fsPath = attachment.FilesystemPath
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if fsPath == "" {
|
2024-01-03 15:56:20 -07:00
|
|
|
return empty, fmt.Errorf("not listed in attachments or scripts: %s", name)
|
2020-09-04 18:28:23 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
return fp.fs.Open(fsPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (fp FsPuzzle) staticPuzzle() (StaticPuzzle, []byte, error) {
|
2020-09-03 20:04:43 -06:00
|
|
|
r, err := fp.fs.Open("puzzle.md")
|
2020-08-28 17:41:17 -06:00
|
|
|
if err != nil {
|
2020-08-31 16:37:51 -06:00
|
|
|
var err2 error
|
2020-09-03 20:04:43 -06:00
|
|
|
if r, err2 = fp.fs.Open("puzzle.moth"); err2 != nil {
|
2020-09-04 18:28:23 -06:00
|
|
|
return StaticPuzzle{}, nil, err
|
2020-08-31 16:37:51 -06:00
|
|
|
}
|
2020-08-28 17:41:17 -06:00
|
|
|
}
|
|
|
|
defer r.Close()
|
|
|
|
|
2019-08-17 11:01:26 -06:00
|
|
|
headerBuf := new(bytes.Buffer)
|
2020-09-01 20:12:57 -06:00
|
|
|
headerParser := rfc822HeaderParser
|
2020-08-28 17:41:17 -06:00
|
|
|
headerEnd := ""
|
2019-08-17 11:01:26 -06:00
|
|
|
|
|
|
|
scanner := bufio.NewScanner(r)
|
|
|
|
lineNo := 0
|
|
|
|
for scanner.Scan() {
|
|
|
|
line := scanner.Text()
|
2020-08-28 17:41:17 -06:00
|
|
|
lineNo++
|
2019-08-17 11:01:26 -06:00
|
|
|
if lineNo == 1 {
|
|
|
|
if line == "---" {
|
2020-09-01 20:12:57 -06:00
|
|
|
headerParser = yamlHeaderParser
|
2019-08-17 11:01:26 -06:00
|
|
|
headerEnd = "---"
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if line == headerEnd {
|
2020-08-28 17:41:17 -06:00
|
|
|
headerBuf.WriteRune('\n')
|
2019-08-17 11:01:26 -06:00
|
|
|
break
|
|
|
|
}
|
|
|
|
headerBuf.WriteString(line)
|
|
|
|
headerBuf.WriteRune('\n')
|
|
|
|
}
|
2019-08-17 13:09:09 -06:00
|
|
|
|
2019-08-17 11:01:26 -06:00
|
|
|
bodyBuf := new(bytes.Buffer)
|
|
|
|
for scanner.Scan() {
|
|
|
|
line := scanner.Text()
|
2020-08-28 17:41:17 -06:00
|
|
|
lineNo++
|
2019-08-17 11:01:26 -06:00
|
|
|
bodyBuf.WriteString(line)
|
|
|
|
bodyBuf.WriteRune('\n')
|
|
|
|
}
|
2019-08-17 13:09:09 -06:00
|
|
|
|
2020-09-04 18:28:23 -06:00
|
|
|
static, err := headerParser(headerBuf)
|
2020-09-01 20:12:57 -06:00
|
|
|
if err != nil {
|
2020-09-04 18:28:23 -06:00
|
|
|
return static, nil, err
|
2019-08-17 11:01:26 -06:00
|
|
|
}
|
2020-08-14 20:26:04 -06:00
|
|
|
|
2021-02-25 15:56:23 -07:00
|
|
|
html := new(bytes.Buffer)
|
|
|
|
err = Markdown(bodyBuf, html)
|
|
|
|
return static, html.Bytes(), err
|
2020-08-28 17:41:17 -06:00
|
|
|
}
|
|
|
|
|
2020-09-04 18:28:23 -06:00
|
|
|
func legacyAttachmentParser(val []string) []StaticAttachment {
|
|
|
|
ret := make([]StaticAttachment, len(val))
|
2020-09-01 20:12:57 -06:00
|
|
|
for idx, txt := range val {
|
|
|
|
parts := strings.SplitN(txt, " ", 3)
|
2020-09-04 18:28:23 -06:00
|
|
|
cur := StaticAttachment{}
|
2020-09-01 20:12:57 -06:00
|
|
|
cur.FilesystemPath = parts[0]
|
|
|
|
if len(parts) > 1 {
|
|
|
|
cur.Filename = parts[1]
|
|
|
|
} else {
|
|
|
|
cur.Filename = cur.FilesystemPath
|
|
|
|
}
|
|
|
|
ret[idx] = cur
|
|
|
|
}
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
2020-09-04 18:28:23 -06:00
|
|
|
func yamlHeaderParser(r io.Reader) (StaticPuzzle, error) {
|
|
|
|
p := StaticPuzzle{}
|
2020-09-01 20:12:57 -06:00
|
|
|
decoder := yaml.NewDecoder(r)
|
|
|
|
decoder.SetStrict(true)
|
|
|
|
err := decoder.Decode(&p)
|
|
|
|
return p, err
|
|
|
|
}
|
|
|
|
|
2020-09-04 18:28:23 -06:00
|
|
|
func rfc822HeaderParser(r io.Reader) (StaticPuzzle, error) {
|
|
|
|
p := StaticPuzzle{}
|
2020-09-01 20:12:57 -06:00
|
|
|
m, err := mail.ReadMessage(r)
|
|
|
|
if err != nil {
|
2024-01-03 15:56:20 -07:00
|
|
|
return p, fmt.Errorf("parsing RFC822 headers: %v", err)
|
2020-09-01 20:12:57 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
for key, val := range m.Header {
|
|
|
|
key = strings.ToLower(key)
|
|
|
|
switch key {
|
|
|
|
case "author":
|
2021-02-24 16:34:35 -07:00
|
|
|
p.Authors = val
|
2020-09-01 20:12:57 -06:00
|
|
|
case "pattern":
|
2021-02-24 16:34:35 -07:00
|
|
|
p.AnswerPattern = val[0]
|
2020-09-01 20:12:57 -06:00
|
|
|
case "script":
|
2021-02-24 16:34:35 -07:00
|
|
|
p.Scripts = legacyAttachmentParser(val)
|
2020-09-01 20:12:57 -06:00
|
|
|
case "file":
|
2021-02-24 16:34:35 -07:00
|
|
|
p.Attachments = legacyAttachmentParser(val)
|
2020-09-01 20:12:57 -06:00
|
|
|
case "answer":
|
|
|
|
p.Answers = val
|
|
|
|
case "summary":
|
|
|
|
p.Debug.Summary = val[0]
|
|
|
|
case "hint":
|
|
|
|
p.Debug.Hints = val
|
2020-09-15 15:58:21 -06:00
|
|
|
case "solution":
|
|
|
|
p.Debug.Hints = val
|
2020-09-01 20:12:57 -06:00
|
|
|
case "ksa":
|
2021-02-24 16:34:35 -07:00
|
|
|
p.KSAs = val
|
2020-09-15 15:58:21 -06:00
|
|
|
case "objective":
|
2021-02-24 16:34:35 -07:00
|
|
|
p.Objective = val[0]
|
2020-09-15 15:58:21 -06:00
|
|
|
case "success.acceptable":
|
2021-02-24 16:34:35 -07:00
|
|
|
p.Success.Acceptable = val[0]
|
2020-09-15 15:58:21 -06:00
|
|
|
case "success.mastery":
|
2021-02-24 16:34:35 -07:00
|
|
|
p.Success.Mastery = val[0]
|
2020-09-01 20:12:57 -06:00
|
|
|
default:
|
2024-01-03 15:56:20 -07:00
|
|
|
return p, fmt.Errorf("unknown header field: %s", key)
|
2020-09-01 20:12:57 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return p, nil
|
2019-08-17 11:01:26 -06:00
|
|
|
}
|
2020-09-03 20:04:43 -06:00
|
|
|
|
2020-09-04 13:00:23 -06:00
|
|
|
// Answer checks whether the given answer is correct.
|
2020-09-03 20:04:43 -06:00
|
|
|
func (fp FsPuzzle) Answer(answer string) bool {
|
2020-09-04 18:28:23 -06:00
|
|
|
p, _, err := fp.staticPuzzle()
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for _, ans := range p.Answers {
|
|
|
|
if ans == answer {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
2020-09-03 20:04:43 -06:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-09-04 13:00:23 -06:00
|
|
|
// FsCommandPuzzle provides an FsPuzzle backed by running a command.
|
2020-09-03 20:04:43 -06:00
|
|
|
type FsCommandPuzzle struct {
|
2020-09-04 13:00:23 -06:00
|
|
|
fs afero.Fs
|
|
|
|
command string
|
|
|
|
timeout time.Duration
|
2020-09-03 20:04:43 -06:00
|
|
|
}
|
|
|
|
|
2020-09-14 18:23:56 -06:00
|
|
|
func (fp FsCommandPuzzle) run(command string, args ...string) ([]byte, error) {
|
2020-09-04 13:00:23 -06:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), fp.timeout)
|
2020-09-03 20:04:43 -06:00
|
|
|
defer cancel()
|
|
|
|
|
2020-09-14 18:23:56 -06:00
|
|
|
cmdargs := append([]string{command}, args...)
|
|
|
|
cmd := exec.CommandContext(ctx, "./"+path.Base(fp.command), cmdargs...)
|
2020-09-11 13:03:19 -06:00
|
|
|
cmd.Dir = path.Dir(fp.command)
|
2020-11-02 14:40:43 -07:00
|
|
|
out, err := cmd.Output()
|
|
|
|
if err, ok := err.(*exec.ExitError); ok {
|
|
|
|
stderr := strings.TrimSpace(string(err.Stderr))
|
|
|
|
return nil, fmt.Errorf("%s (%s)", stderr, err.String())
|
|
|
|
}
|
|
|
|
return out, err
|
2020-09-14 13:40:55 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// Puzzle returns a Puzzle struct for the current puzzle.
|
|
|
|
func (fp FsCommandPuzzle) Puzzle() (Puzzle, error) {
|
2020-09-14 18:23:56 -06:00
|
|
|
stdout, err := fp.run("puzzle")
|
2020-09-11 17:33:43 -06:00
|
|
|
if exiterr, ok := err.(*exec.ExitError); ok {
|
|
|
|
return Puzzle{}, errors.New(string(exiterr.Stderr))
|
|
|
|
} else if err != nil {
|
2020-09-03 20:04:43 -06:00
|
|
|
return Puzzle{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
jsdec := json.NewDecoder(bytes.NewReader(stdout))
|
|
|
|
jsdec.DisallowUnknownFields()
|
|
|
|
puzzle := Puzzle{}
|
|
|
|
if err := jsdec.Decode(&puzzle); err != nil {
|
|
|
|
return Puzzle{}, err
|
|
|
|
}
|
|
|
|
|
2020-09-04 18:28:23 -06:00
|
|
|
puzzle.computeAnswerHashes()
|
|
|
|
|
2020-09-03 20:04:43 -06:00
|
|
|
return puzzle, nil
|
|
|
|
}
|
|
|
|
|
2020-09-08 17:49:02 -06:00
|
|
|
type nopCloser struct {
|
|
|
|
io.ReadSeeker
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c nopCloser) Close() error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-09-04 13:00:23 -06:00
|
|
|
// Open returns a newly-opened file.
|
2020-09-08 17:49:02 -06:00
|
|
|
// BUG(neale): FsCommandPuzzle.Open() reads everything into memory, and will suck for large files.
|
|
|
|
func (fp FsCommandPuzzle) Open(filename string) (ReadSeekCloser, error) {
|
2020-09-14 18:23:56 -06:00
|
|
|
stdout, err := fp.run("file", filename)
|
2020-09-14 13:40:55 -06:00
|
|
|
buf := nopCloser{bytes.NewReader(stdout)}
|
2020-09-04 13:00:23 -06:00
|
|
|
if err != nil {
|
2020-09-04 15:29:06 -06:00
|
|
|
return buf, err
|
2020-09-04 13:00:23 -06:00
|
|
|
}
|
|
|
|
|
2020-09-04 15:29:06 -06:00
|
|
|
return buf, nil
|
2020-09-03 20:04:43 -06:00
|
|
|
}
|
|
|
|
|
2020-09-04 13:00:23 -06:00
|
|
|
// Answer checks whether the given answer is correct.
|
2020-09-03 20:04:43 -06:00
|
|
|
func (fp FsCommandPuzzle) Answer(answer string) bool {
|
2020-09-14 18:23:56 -06:00
|
|
|
stdout, err := fp.run("answer", answer)
|
2020-09-04 13:00:23 -06:00
|
|
|
if err != nil {
|
2020-09-04 15:29:06 -06:00
|
|
|
log.Printf("ERROR: checking answer: %s", err)
|
2020-09-04 13:00:23 -06:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-10-16 14:18:44 -06:00
|
|
|
ans := AnswerResponse{}
|
|
|
|
if err := json.Unmarshal(stdout, &ans); err != nil {
|
|
|
|
log.Printf("ERROR: checking answer: %s", err)
|
|
|
|
return false
|
2020-09-04 13:00:23 -06:00
|
|
|
}
|
2020-10-16 14:18:44 -06:00
|
|
|
|
|
|
|
return ans.Correct
|
2020-09-03 20:04:43 -06:00
|
|
|
}
|