/
iteration.go
195 lines (159 loc) · 5.47 KB
/
iteration.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
package api
import (
"bytes"
"errors"
"io/ioutil"
"os"
"path/filepath"
"strings"
"text/template"
"golang.org/x/net/html/charset"
"golang.org/x/text/transform"
)
const (
mimeType = "text/plain"
)
var (
errNoFiles = errors.New("no files submitted")
utf8BOM = []byte{0xef, 0xbb, 0xbf}
)
var msgSubmitCalledFromWrongDir = `Unable to identify track and file.
It seems like you've tried to submit a solution file located outside of your
configured exercises directory.
Current directory: {{ .Current }}
Configured directory: {{ .Configured }}
Try re-running "exercism fetch". Then move your solution file to the correct
exercise directory for the problem you're working on. It should be somewhere
inside {{ .Configured }}
For example, to submit the JavaScript "hello-world.js" problem, run
"exercism submit hello-world.js" from this directory:
{{ .Configured }}{{ .Separator }}javascript{{ .Separator }}hello-world
You can see where exercism is looking for your files with "exercism debug".
`
var msgGenericPathError = `Bad path to exercise file.
You're trying to submit a solution file from inside your exercises directory,
but it looks like the directory structure is something that exercism doesn't
recognize as a valid file path.
First, make a copy of your solution file and save it outside of
{{ .Configured }}
Then, run "exercism fetch". Move your solution file back to the correct
exercise directory for the problem you're working on. It should be somewhere
inside {{ .Configured }}
If you are having trouble, you can file a GitHub issue at (https://github.com/exercism/exercism.io/issues)
`
// Iteration represents a version of a particular exercise.
// This gets submitted to the API.
type Iteration struct {
Key string `json:"key"`
Code string `json:"code"`
Dir string `json:"dir"`
TrackID string `json:"language"`
Problem string `json:"problem"`
Solution map[string]string `json:"solution"`
Comment string `json:"comment,omitempty"`
}
// NewIteration prepares an iteration of a problem in a track for submission to the API.
// It takes a dir (from the global config) and a list of files which it will read from disk.
// Paths can point to regular files or to symlinks.
func NewIteration(dir string, filenames []string) (*Iteration, error) {
if len(filenames) == 0 {
return nil, errNoFiles
}
iter := &Iteration{
Dir: dir,
Solution: map[string]string{},
}
// All the files should be within the exercism path.
for _, filename := range filenames {
if !iter.isValidFilepath(filename) {
// User has run exercism submit in the wrong directory.
return nil, newIterationError(msgSubmitCalledFromWrongDir, iter.Dir)
}
}
// Identify the language track and problem slug.
path := filenames[0][len(dir):]
segments := strings.Split(path, string(filepath.Separator))
if len(segments) < 4 {
// Submit was called from inside exercism directory, but the path
// is still bad. Has the user modified their path in some way?
return nil, newIterationError(msgGenericPathError, iter.Dir)
}
iter.TrackID = strings.ToLower(segments[1])
iter.Problem = strings.ToLower(segments[2])
for _, filename := range filenames {
fileContents, err := readFileAsUTF8String(filename)
if err != nil {
return nil, err
}
path := filename[len(iter.RelativePath()):]
iter.Solution[path] = *fileContents
}
return iter, nil
}
// RelativePath returns the iteration's relative path.
func (iter *Iteration) RelativePath() string {
return filepath.Join(iter.Dir, iter.TrackID, iter.Problem) + string(filepath.Separator)
}
// isValidFilepath checks a files's absolute filepath and returns true if it is
// within the configured exercise directory.
func (iter *Iteration) isValidFilepath(path string) bool {
if iter == nil {
return false
}
return strings.HasPrefix(strings.ToLower(path), strings.ToLower(iter.Dir))
}
func readFileAsUTF8String(filename string) (*string, error) {
b, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
encoding, _, certain := charset.DetermineEncoding(b, mimeType)
if !certain {
// We don't want to use an uncertain encoding.
// In particular, doing that may mangle UTF-8 files
// that have only ASCII in their first 1024 bytes.
// See https://github.com/exercism/cli/issues/309.
// So if we're unsure, use UTF-8 (no transformation).
s := string(b)
return &s, nil
}
decoder := encoding.NewDecoder()
decodedBytes, _, err := transform.Bytes(decoder, b)
if err != nil {
return nil, err
}
// Drop the UTF-8 BOM that may have been added. This isn't necessary, and
// it's going to be written into another UTF-8 buffer anyway once it's JSON
// serialized.
//
// The standard recommends omitting the BOM. See
// http://www.unicode.org/versions/Unicode5.0.0/ch02.pdf
decodedBytes = bytes.TrimPrefix(decodedBytes, utf8BOM)
s := string(decodedBytes)
return &s, nil
}
// newIterationError executes an error message template to create a detailed
// message for the end user. An error type is returned.
func newIterationError(msgTemplate, configured string) error {
buffer := bytes.NewBufferString("")
t, err := template.New("iterErr").Parse(msgTemplate)
if err != nil {
return err
}
current, err := os.Getwd()
if err != nil {
return err
}
var pathData = struct {
Current string
Configured string
Separator string
}{
current,
configured,
string(filepath.Separator),
}
t.Execute(buffer, pathData)
msg := buffer.String()
return errors.New(msg)
}