sync: Replace []error by go.uber.org/multierr

This commit is contained in:
nemunaire 2023-11-22 12:16:53 +01:00
parent 9f49a689fd
commit b6966d47ce
25 changed files with 380 additions and 348 deletions

View File

@ -13,10 +13,11 @@ import (
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/multierr"
) )
func flatifySyncErrors(errs []error) (ret []string) { func flatifySyncErrors(errs error) (ret []string) {
for _, err := range errs { for _, err := range multierr.Errors(errs) {
ret = append(ret, err.Error()) ret = append(ret, err.Error())
} }
return return
@ -62,7 +63,7 @@ func declareSyncRoutes(router *gin.RouterGroup) {
exceptions := sync.LoadThemeException(sync.GlobalImporter, theme) exceptions := sync.LoadThemeException(sync.GlobalImporter, theme)
var st []string var st []string
for _, se := range sync.SyncThemeDeep(sync.GlobalImporter, theme, 0, 250, exceptions) { for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, theme, 0, 250, exceptions)) {
st = append(st, se.Error()) st = append(st, se.Error())
} }
sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theme.Name: st}}, false) sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theme.Name: st}}, false)
@ -195,7 +196,7 @@ func declareSyncExercicesRoutes(router *gin.RouterGroup) {
exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil) exceptions := sync.LoadExerciceException(sync.GlobalImporter, theme, exercice, nil)
_, errs := sync.SyncExerciceFlags(sync.GlobalImporter, exercice, exceptions) _, errs := sync.SyncExerciceFlags(sync.GlobalImporter, exercice, exceptions)
_, herrs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice), exceptions) _, herrs := sync.SyncExerciceHints(sync.GlobalImporter, exercice, sync.ExerciceFlagsMap(sync.GlobalImporter, exercice), exceptions)
c.JSON(http.StatusOK, flatifySyncErrors(append(errs, herrs...))) c.JSON(http.StatusOK, flatifySyncErrors(multierr.Append(errs, herrs)))
}) })
} }
@ -286,7 +287,7 @@ func autoSync(c *gin.Context) {
exceptions := sync.LoadThemeException(sync.GlobalImporter, theTheme) exceptions := sync.LoadThemeException(sync.GlobalImporter, theTheme)
var st []string var st []string
for _, se := range sync.SyncThemeDeep(sync.GlobalImporter, theTheme, 0, 250, exceptions) { for _, se := range multierr.Errors(sync.SyncThemeDeep(sync.GlobalImporter, theTheme, 0, 250, exceptions)) {
st = append(st, se.Error()) st = append(st, se.Error())
} }
sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theTheme.Name: st}}, false) sync.EditDeepReport(&sync.SyncReport{Themes: map[string][]string{theTheme.Name: st}}, false)

View File

@ -5,6 +5,8 @@ import (
"path" "path"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
) )
@ -130,12 +132,12 @@ func parseExerciceParams(i Importer, exPath string) (p ExerciceParams, md toml.M
} }
// getExerciceParams returns normalized // getExerciceParams returns normalized
func getExerciceParams(i Importer, exercice *fic.Exercice) (params ExerciceParams, errs []error) { func getExerciceParams(i Importer, exercice *fic.Exercice) (params ExerciceParams, errs error) {
var err error var err error
if params, _, err = parseExerciceParams(i, exercice.Path); err != nil { if params, _, err = parseExerciceParams(i, exercice.Path); err != nil {
errs = append(errs, NewChallengeTxtError(exercice, 0, err)) errs = multierr.Append(errs, NewChallengeTxtError(exercice, 0, err))
} else if len(params.Flags) == 0 && len(params.FlagsUCQ) == 0 && len(params.FlagsMCQ) == 0 { } else if len(params.Flags) == 0 && len(params.FlagsUCQ) == 0 && len(params.FlagsMCQ) == 0 {
errs = append(errs, NewChallengeTxtError(exercice, 0, fmt.Errorf("has no flag"))) errs = multierr.Append(errs, NewChallengeTxtError(exercice, 0, fmt.Errorf("has no flag")))
} else { } else {
// Treat legacy UCQ flags as ExerciceFlag // Treat legacy UCQ flags as ExerciceFlag
for _, flag := range params.FlagsUCQ { for _, flag := range params.FlagsUCQ {

View File

@ -14,6 +14,7 @@ import (
"unicode" "unicode"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
) )
@ -50,7 +51,7 @@ func isURLAllowed(in string) bool {
return false return false
} }
func BuildFilesListInto(i Importer, exercice *fic.Exercice, into string) (files []string, digests map[string][]byte, errs []error) { func BuildFilesListInto(i Importer, exercice *fic.Exercice, into string) (files []string, digests map[string][]byte, errs error) {
// If no files directory, don't display error // If no files directory, don't display error
if !i.Exists(path.Join(exercice.Path, into)) { if !i.Exists(path.Join(exercice.Path, into)) {
return return
@ -58,15 +59,15 @@ func BuildFilesListInto(i Importer, exercice *fic.Exercice, into string) (files
// Parse DIGESTS.txt // Parse DIGESTS.txt
if digs, err := GetFileContent(i, path.Join(exercice.Path, into, "DIGESTS.txt")); err != nil { if digs, err := GetFileContent(i, path.Join(exercice.Path, into, "DIGESTS.txt")); err != nil {
errs = append(errs, NewExerciceError(exercice, fmt.Errorf("unable to read %s: %w", path.Join(into, "DIGESTS.txt"), err))) errs = multierr.Append(errs, NewExerciceError(exercice, fmt.Errorf("unable to read %s: %w", path.Join(into, "DIGESTS.txt"), err)))
} else { } else {
digests = map[string][]byte{} digests = map[string][]byte{}
for nline, d := range strings.Split(digs, "\n") { for nline, d := range strings.Split(digs, "\n") {
if dsplt := strings.SplitN(d, " ", 2); len(dsplt) < 2 { if dsplt := strings.SplitN(d, " ", 2); len(dsplt) < 2 {
errs = append(errs, NewExerciceError(exercice, fmt.Errorf("unable to parse %s line %d: invalid format", path.Join(into, "DIGESTS.txt"), nline+1))) errs = multierr.Append(errs, NewExerciceError(exercice, fmt.Errorf("unable to parse %s line %d: invalid format", path.Join(into, "DIGESTS.txt"), nline+1)))
continue continue
} else if hash, err := hex.DecodeString(dsplt[0]); err != nil { } else if hash, err := hex.DecodeString(dsplt[0]); err != nil {
errs = append(errs, NewExerciceError(exercice, fmt.Errorf("unable to parse %s line %d: %w", path.Join(into, "DIGESTS.txt"), nline+1, err))) errs = multierr.Append(errs, NewExerciceError(exercice, fmt.Errorf("unable to parse %s line %d: %w", path.Join(into, "DIGESTS.txt"), nline+1, err)))
continue continue
} else { } else {
digests[strings.TrimFunc(dsplt[1], unicode.IsSpace)] = hash digests[strings.TrimFunc(dsplt[1], unicode.IsSpace)] = hash
@ -76,7 +77,7 @@ func BuildFilesListInto(i Importer, exercice *fic.Exercice, into string) (files
// Read file list // Read file list
if flist, err := i.listDir(path.Join(exercice.Path, into)); err != nil { if flist, err := i.listDir(path.Join(exercice.Path, into)); err != nil {
errs = append(errs, NewExerciceError(exercice, err)) errs = multierr.Append(errs, NewExerciceError(exercice, err))
} else { } else {
for _, fname := range flist { for _, fname := range flist {
if fname == "DIGESTS.txt" || fname == ".gitattributes" { if fname == "DIGESTS.txt" || fname == ".gitattributes" {
@ -128,9 +129,9 @@ func BuildFilesListInto(i Importer, exercice *fic.Exercice, into string) (files
} }
// CheckExerciceFilesPresence limits remote checks to presence, don't get it to check digest. // CheckExerciceFilesPresence limits remote checks to presence, don't get it to check digest.
func CheckExerciceFilesPresence(i Importer, exercice *fic.Exercice) (files []string, errs []error) { func CheckExerciceFilesPresence(i Importer, exercice *fic.Exercice) (files []string, errs error) {
flist, digests, berrs := BuildFilesListInto(i, exercice, "files") flist, digests, berrs := BuildFilesListInto(i, exercice, "files")
errs = append(errs, berrs...) errs = multierr.Append(errs, berrs)
paramsFiles, _ := GetExerciceFilesParams(i, exercice) paramsFiles, _ := GetExerciceFilesParams(i, exercice)
@ -138,28 +139,28 @@ func CheckExerciceFilesPresence(i Importer, exercice *fic.Exercice) (files []str
if !i.Exists(path.Join(exercice.Path, "files", fname)) && !i.Exists(path.Join(exercice.Path, "files", fname+".00")) { if !i.Exists(path.Join(exercice.Path, "files", fname)) && !i.Exists(path.Join(exercice.Path, "files", fname+".00")) {
// File not found locally, is this a remote file? // File not found locally, is this a remote file?
if pf, exists := paramsFiles[fname]; !exists || pf.URL == "" { if pf, exists := paramsFiles[fname]; !exists || pf.URL == "" {
errs = append(errs, NewFileError(exercice, fname, fmt.Errorf("No such file or directory"))) errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("No such file or directory")))
continue continue
} else if !isURLAllowed(pf.URL) { } else if !isURLAllowed(pf.URL) {
errs = append(errs, NewFileError(exercice, fname, fmt.Errorf("URL hostname is not whitelisted"))) errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("URL hostname is not whitelisted")))
continue continue
} else { } else {
resp, err := http.Head(pf.URL) resp, err := http.Head(pf.URL)
if err != nil { if err != nil {
errs = append(errs, NewFileError(exercice, fname, err)) errs = multierr.Append(errs, NewFileError(exercice, fname, err))
continue continue
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode >= 300 { if resp.StatusCode >= 300 {
errs = append(errs, NewFileError(exercice, fname, fmt.Errorf("Unexpected status code for the HTTP response: %d %s", resp.StatusCode, resp.Status))) errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("Unexpected status code for the HTTP response: %d %s", resp.StatusCode, resp.Status)))
continue continue
} }
} }
} }
if _, ok := digests[fname]; !ok { if _, ok := digests[fname]; !ok {
errs = append(errs, NewFileError(exercice, fname, fmt.Errorf("unable to import file: No digest given"))) errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("unable to import file: No digest given")))
} else { } else {
files = append(files, fname) files = append(files, fname)
} }
@ -169,7 +170,7 @@ func CheckExerciceFilesPresence(i Importer, exercice *fic.Exercice) (files []str
if !i.Exists(path.Join(exercice.Path, "files", fname)) && !i.Exists(path.Join(exercice.Path, "files", fname+".gz")) && !i.Exists(path.Join(exercice.Path, "files", fname+".00")) && !i.Exists(path.Join(exercice.Path, "files", fname+".gz.00")) { if !i.Exists(path.Join(exercice.Path, "files", fname)) && !i.Exists(path.Join(exercice.Path, "files", fname+".gz")) && !i.Exists(path.Join(exercice.Path, "files", fname+".00")) && !i.Exists(path.Join(exercice.Path, "files", fname+".gz.00")) {
if pf, exists := paramsFiles[fname]; !exists || pf.URL == "" { if pf, exists := paramsFiles[fname]; !exists || pf.URL == "" {
if pf, exists := paramsFiles[fname+".gz"]; !exists || pf.URL == "" { if pf, exists := paramsFiles[fname+".gz"]; !exists || pf.URL == "" {
errs = append(errs, NewFileError(exercice, fname, fmt.Errorf("unable to read file: No such file or directory. Check your DIGESTS.txt for legacy entries."))) errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("unable to read file: No such file or directory. Check your DIGESTS.txt for legacy entries.")))
} }
} }
} }
@ -179,13 +180,13 @@ func CheckExerciceFilesPresence(i Importer, exercice *fic.Exercice) (files []str
} }
// CheckExerciceFiles checks that remote files have the right digest. // CheckExerciceFiles checks that remote files have the right digest.
func CheckExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (files []string, errs []error) { func CheckExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (files []string, errs error) {
flist, digests, berrs := BuildFilesListInto(i, exercice, "files") flist, digests, berrs := BuildFilesListInto(i, exercice, "files")
errs = append(errs, berrs...) errs = multierr.Append(errs, berrs)
paramsFiles, err := GetExerciceFilesParams(i, exercice) paramsFiles, err := GetExerciceFilesParams(i, exercice)
if err != nil { if err != nil {
errs = append(errs, NewChallengeTxtError(exercice, 0, err)) errs = multierr.Append(errs, NewChallengeTxtError(exercice, 0, err))
} }
for _, fname := range flist { for _, fname := range flist {
@ -193,14 +194,14 @@ func CheckExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExc
if pf, exists := paramsFiles[fname]; exists && pf.URL != "" { if pf, exists := paramsFiles[fname]; exists && pf.URL != "" {
if li, ok := i.(LocalImporter); ok { if li, ok := i.(LocalImporter); ok {
errs = append(errs, downloadExerciceFile(paramsFiles[fname], li.GetLocalPath(dest), exercice, false)...) errs = multierr.Append(errs, downloadExerciceFile(paramsFiles[fname], li.GetLocalPath(dest), exercice, false))
} else { } else {
errs = append(errs, downloadExerciceFile(paramsFiles[fname], dest, exercice, false)...) errs = multierr.Append(errs, downloadExerciceFile(paramsFiles[fname], dest, exercice, false))
} }
} }
if fd, closer, err := GetFile(i, dest); err != nil { if fd, closer, err := GetFile(i, dest); err != nil {
errs = append(errs, NewFileError(exercice, fname, fmt.Errorf("unable to read file: %w", err))) errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("unable to read file: %w", err)))
continue continue
} else { } else {
defer closer() defer closer()
@ -208,9 +209,9 @@ func CheckExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExc
hash160, hash512 := fic.CreateHashBuffers(fd) hash160, hash512 := fic.CreateHashBuffers(fd)
if _, err := fic.CheckBufferHash(hash160, hash512, digests[fname]); err != nil { if _, err := fic.CheckBufferHash(hash160, hash512, digests[fname]); err != nil {
errs = append(errs, NewFileError(exercice, fname, err)) errs = multierr.Append(errs, NewFileError(exercice, fname, err))
} else if size, err := GetFileSize(i, path.Join(exercice.Path, "files", fname)); err != nil { } else if size, err := GetFileSize(i, path.Join(exercice.Path, "files", fname)); err != nil {
errs = append(errs, NewFileError(exercice, fname, err)) errs = multierr.Append(errs, NewFileError(exercice, fname, err))
} else { } else {
var digest_shown []byte var digest_shown []byte
if strings.HasSuffix(fname, ".gz") { if strings.HasSuffix(fname, ".gz") {
@ -219,11 +220,11 @@ func CheckExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExc
// Check that gunzipped file digest is correct // Check that gunzipped file digest is correct
if fd, closer, err := GetFile(i, path.Join(exercice.Path, "files", fname)); err != nil { if fd, closer, err := GetFile(i, path.Join(exercice.Path, "files", fname)); err != nil {
errs = append(errs, NewFileError(exercice, fname, fmt.Errorf("unable to read file: %w", err))) errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("unable to read file: %w", err)))
continue continue
} else if gunzipfd, err := gzip.NewReader(fd); err != nil { } else if gunzipfd, err := gzip.NewReader(fd); err != nil {
closer() closer()
errs = append(errs, NewFileError(exercice, fname, fmt.Errorf("unable to gunzip file: %w", err))) errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("unable to gunzip file: %w", err)))
continue continue
} else { } else {
defer gunzipfd.Close() defer gunzipfd.Close()
@ -232,7 +233,7 @@ func CheckExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExc
hash160_inflate, hash512_inflate := fic.CreateHashBuffers(gunzipfd) hash160_inflate, hash512_inflate := fic.CreateHashBuffers(gunzipfd)
if _, err := fic.CheckBufferHash(hash160_inflate, hash512_inflate, digest_shown); err != nil { if _, err := fic.CheckBufferHash(hash160_inflate, hash512_inflate, digest_shown); err != nil {
errs = append(errs, NewFileError(exercice, strings.TrimSuffix(fname, ".gz"), err)) errs = multierr.Append(errs, NewFileError(exercice, strings.TrimSuffix(fname, ".gz"), err))
} }
} }
} }
@ -242,13 +243,13 @@ func CheckExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExc
if f, exists := paramsFiles[fname]; exists { if f, exists := paramsFiles[fname]; exists {
// Call checks hooks // Call checks hooks
for _, hk := range hooks.mdTextHooks { for _, hk := range hooks.mdTextHooks {
for _, err := range hk(f.Disclaimer, exercice.Language, exceptions) { for _, err := range multierr.Errors(hk(f.Disclaimer, exercice.Language, exceptions)) {
errs = append(errs, NewFileError(exercice, fname, err)) errs = multierr.Append(errs, NewFileError(exercice, fname, err))
} }
} }
if disclaimer, err = ProcessMarkdown(i, fixnbsp(f.Disclaimer), exercice.Path); err != nil { if disclaimer, err = ProcessMarkdown(i, fixnbsp(f.Disclaimer), exercice.Path); err != nil {
errs = append(errs, NewFileError(exercice, fname, fmt.Errorf("error during markdown formating of disclaimer: %w", err))) errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("error during markdown formating of disclaimer: %w", err)))
} }
} }
@ -256,8 +257,8 @@ func CheckExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExc
// Call checks hooks // Call checks hooks
for _, h := range hooks.fileHooks { for _, h := range hooks.fileHooks {
for _, e := range h(file, exercice, exceptions) { for _, e := range multierr.Errors(h(file, exercice, exceptions)) {
errs = append(errs, NewFileError(exercice, fname, e)) errs = multierr.Append(errs, NewFileError(exercice, fname, e))
} }
} }
} }
@ -269,7 +270,7 @@ func CheckExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExc
} }
// downloadExerciceFile is responsible to fetch remote files. // downloadExerciceFile is responsible to fetch remote files.
func downloadExerciceFile(pf ExerciceFile, dest string, exercice *fic.Exercice, force bool) (errs []error) { func downloadExerciceFile(pf ExerciceFile, dest string, exercice *fic.Exercice, force bool) (errs error) {
if st, err := os.Stat(dest); !force && !os.IsNotExist(err) { if st, err := os.Stat(dest); !force && !os.IsNotExist(err) {
resp, err := http.Head(pf.URL) resp, err := http.Head(pf.URL)
if err == nil && resp.ContentLength == st.Size() { if err == nil && resp.ContentLength == st.Size() {
@ -278,33 +279,35 @@ func downloadExerciceFile(pf ExerciceFile, dest string, exercice *fic.Exercice,
} }
if !isURLAllowed(pf.URL) { if !isURLAllowed(pf.URL) {
errs = append(errs, NewFileError(exercice, path.Base(dest), fmt.Errorf("URL hostname is not whitelisted"))) errs = multierr.Append(errs, NewFileError(exercice, path.Base(dest), fmt.Errorf("URL hostname is not whitelisted")))
return return
} }
log.Println("Download exercice file: ", pf.URL)
resp, err := http.Get(pf.URL) resp, err := http.Get(pf.URL)
if err != nil { if err != nil {
errs = append(errs, NewFileError(exercice, path.Base(dest), err)) errs = multierr.Append(errs, NewFileError(exercice, path.Base(dest), err))
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
if err = os.MkdirAll(path.Dir(dest), 0751); err != nil { if err = os.MkdirAll(path.Dir(dest), 0751); err != nil {
errs = append(errs, NewFileError(exercice, path.Base(dest), err)) errs = multierr.Append(errs, NewFileError(exercice, path.Base(dest), err))
return return
} }
// Write file // Write file
var fdto *os.File var fdto *os.File
if fdto, err = os.Create(dest); err != nil { if fdto, err = os.Create(dest); err != nil {
errs = append(errs, NewFileError(exercice, path.Base(dest), err)) errs = multierr.Append(errs, NewFileError(exercice, path.Base(dest), err))
return return
} else { } else {
defer fdto.Close() defer fdto.Close()
_, err = io.Copy(fdto, resp.Body) _, err = io.Copy(fdto, resp.Body)
if err != nil { if err != nil {
errs = append(errs, NewFileError(exercice, path.Base(dest), err)) errs = multierr.Append(errs, NewFileError(exercice, path.Base(dest), err))
return return
} }
} }
@ -314,19 +317,19 @@ func downloadExerciceFile(pf ExerciceFile, dest string, exercice *fic.Exercice,
// SyncExerciceFiles reads the content of files/ directory and import it as EFile for the given challenge. // SyncExerciceFiles reads the content of files/ directory and import it as EFile for the given challenge.
// It takes care of DIGESTS.txt and ensure imported files match. // It takes care of DIGESTS.txt and ensure imported files match.
func SyncExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (errs []error) { func SyncExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (errs error) {
if _, err := exercice.WipeFiles(); err != nil { if _, err := exercice.WipeFiles(); err != nil {
errs = append(errs, err) errs = multierr.Append(errs, err)
} }
paramsFiles, err := GetExerciceFilesParams(i, exercice) paramsFiles, err := GetExerciceFilesParams(i, exercice)
if err != nil { if err != nil {
errs = append(errs, NewChallengeTxtError(exercice, 0, err)) errs = multierr.Append(errs, NewChallengeTxtError(exercice, 0, err))
return return
} }
files, digests, berrs := BuildFilesListInto(i, exercice, "files") files, digests, berrs := BuildFilesListInto(i, exercice, "files")
errs = append(errs, berrs...) errs = multierr.Append(errs, berrs)
// Import standard files // Import standard files
for _, fname := range files { for _, fname := range files {
@ -345,13 +348,13 @@ func SyncExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExce
// Call checks hooks // Call checks hooks
for _, hk := range hooks.mdTextHooks { for _, hk := range hooks.mdTextHooks {
for _, err := range hk(f.Disclaimer, exercice.Language, exceptions) { for _, err := range multierr.Errors(hk(f.Disclaimer, exercice.Language, exceptions)) {
errs = append(errs, NewFileError(exercice, fname, err)) errs = multierr.Append(errs, NewFileError(exercice, fname, err))
} }
} }
if disclaimer, err = ProcessMarkdown(i, fixnbsp(f.Disclaimer), exercice.Path); err != nil { if disclaimer, err = ProcessMarkdown(i, fixnbsp(f.Disclaimer), exercice.Path); err != nil {
errs = append(errs, NewFileError(exercice, fname, fmt.Errorf("error during markdown formating of disclaimer: %w", err))) errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("error during markdown formating of disclaimer: %w", err)))
} }
} }
@ -370,7 +373,7 @@ func SyncExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExce
} }
if f == nil { if f == nil {
errs = append(errs, downloadExerciceFile(paramsFiles[fname], dest, exercice, false)...) errs = multierr.Append(errs, downloadExerciceFile(paramsFiles[fname], dest, exercice, false))
f, err = actionAfterImport(dest, pf.URL) f, err = actionAfterImport(dest, pf.URL)
} }
@ -379,19 +382,19 @@ func SyncExerciceFiles(i Importer, exercice *fic.Exercice, exceptions *CheckExce
} }
if err != nil { if err != nil {
errs = append(errs, NewFileError(exercice, fname, err)) errs = multierr.Append(errs, NewFileError(exercice, fname, err))
continue continue
} }
if f.(*fic.EFile).Size == 0 { if f.(*fic.EFile).Size == 0 {
errs = append(errs, NewFileError(exercice, fname, fmt.Errorf("imported file is empty!"))) errs = multierr.Append(errs, NewFileError(exercice, fname, fmt.Errorf("imported file is empty!")))
} else { } else {
file := f.(*fic.EFile) file := f.(*fic.EFile)
// Call checks hooks // Call checks hooks
for _, h := range hooks.fileHooks { for _, h := range hooks.fileHooks {
for _, e := range h(file, exercice, exceptions) { for _, e := range multierr.Errors(h(file, exercice, exceptions)) {
errs = append(errs, NewFileError(exercice, fname, e)) errs = multierr.Append(errs, NewFileError(exercice, fname, e))
} }
} }

View File

@ -12,6 +12,7 @@ import (
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/multierr"
_ "golang.org/x/crypto/blake2b" _ "golang.org/x/crypto/blake2b"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
@ -23,10 +24,10 @@ type importHint struct {
FlagsDeps []int64 FlagsDeps []int64
} }
func buildExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (hints []importHint, errs []error) { func buildExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (hints []importHint, errs error) {
params, _, err := parseExerciceParams(i, exercice.Path) params, _, err := parseExerciceParams(i, exercice.Path)
if err != nil { if err != nil {
errs = append(errs, NewChallengeTxtError(exercice, 0, err)) errs = multierr.Append(errs, NewChallengeTxtError(exercice, 0, err))
return return
} }
@ -45,10 +46,10 @@ func buildExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExc
if hint.Filename != "" { if hint.Filename != "" {
if hint.Content != "" { if hint.Content != "" {
errs = append(errs, NewHintError(exercice, h, n, fmt.Errorf("content and filename can't be filled at the same time"))) errs = multierr.Append(errs, NewHintError(exercice, h, n, fmt.Errorf("content and filename can't be filled at the same time")))
continue continue
} else if !i.Exists(path.Join(exercice.Path, "hints", hint.Filename)) { } else if !i.Exists(path.Join(exercice.Path, "hints", hint.Filename)) {
errs = append(errs, NewHintError(exercice, h, n, fmt.Errorf("%q: File not found", hint.Filename))) errs = multierr.Append(errs, NewHintError(exercice, h, n, fmt.Errorf("%q: File not found", hint.Filename)))
continue continue
} else { } else {
// Handle files as downloadable content // Handle files as downloadable content
@ -70,35 +71,35 @@ func buildExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExc
// Special format for downloadable hints: $FILES + hexhash + path from FILES/ // Special format for downloadable hints: $FILES + hexhash + path from FILES/
return "$FILES" + hex.EncodeToString(result512) + strings.TrimPrefix(filePath, fic.FilesDir), nil return "$FILES" + hex.EncodeToString(result512) + strings.TrimPrefix(filePath, fic.FilesDir), nil
}); err != nil { }); err != nil {
errs = append(errs, NewHintError(exercice, h, n, fmt.Errorf("%q: unable to import hint file: %w", hint.Filename, err))) errs = multierr.Append(errs, NewHintError(exercice, h, n, fmt.Errorf("%q: unable to import hint file: %w", hint.Filename, err)))
continue continue
} else if s, ok := res.(string); !ok { } else if s, ok := res.(string); !ok {
errs = append(errs, NewHintError(exercice, h, n, fmt.Errorf("%q: unable to import hint file: invalid string returned as filename", hint.Filename))) errs = multierr.Append(errs, NewHintError(exercice, h, n, fmt.Errorf("%q: unable to import hint file: invalid string returned as filename", hint.Filename)))
continue continue
} else { } else {
h.Content = s h.Content = s
} }
} }
} else if hint.Content == "" { } else if hint.Content == "" {
errs = append(errs, NewHintError(exercice, h, n, fmt.Errorf("content and filename can't be empty at the same time"))) errs = multierr.Append(errs, NewHintError(exercice, h, n, fmt.Errorf("content and filename can't be empty at the same time")))
continue continue
} else { } else {
// Call checks hooks // Call checks hooks
for _, hk := range hooks.mdTextHooks { for _, hk := range hooks.mdTextHooks {
for _, err := range hk(h.Content, exercice.Language, exceptions) { for _, err := range multierr.Errors(hk(h.Content, exercice.Language, exceptions)) {
errs = append(errs, NewHintError(exercice, h, n, err)) errs = multierr.Append(errs, NewHintError(exercice, h, n, err))
} }
} }
if h.Content, err = ProcessMarkdown(i, fixnbsp(hint.Content), exercice.Path); err != nil { if h.Content, err = ProcessMarkdown(i, fixnbsp(hint.Content), exercice.Path); err != nil {
errs = append(errs, NewHintError(exercice, h, n, fmt.Errorf("error during markdown formating: %w", err))) errs = multierr.Append(errs, NewHintError(exercice, h, n, fmt.Errorf("error during markdown formating: %w", err)))
} }
} }
// Call checks hooks // Call checks hooks
for _, hook := range hooks.hintHooks { for _, hook := range hooks.hintHooks {
for _, e := range hook(h, exercice, exceptions) { for _, e := range multierr.Errors(hook(h, exercice, exceptions)) {
errs = append(errs, NewHintError(exercice, h, n, e)) errs = multierr.Append(errs, NewHintError(exercice, h, n, e))
} }
} }
@ -119,28 +120,28 @@ func buildExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExc
} }
// CheckExerciceHints checks if all hints are corrects.. // CheckExerciceHints checks if all hints are corrects..
func CheckExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) ([]importHint, []error) { func CheckExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) ([]importHint, error) {
exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml") exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")
return buildExerciceHints(i, exercice, exceptions) return buildExerciceHints(i, exercice, exceptions)
} }
// SyncExerciceHints reads the content of hints/ directories and import it as EHint for the given challenge. // SyncExerciceHints reads the content of hints/ directories and import it as EHint for the given challenge.
func SyncExerciceHints(i Importer, exercice *fic.Exercice, flagsBindings map[int64]fic.Flag, exceptions *CheckExceptions) (hintsBindings map[int]*fic.EHint, errs []error) { func SyncExerciceHints(i Importer, exercice *fic.Exercice, flagsBindings map[int64]fic.Flag, exceptions *CheckExceptions) (hintsBindings map[int]*fic.EHint, errs error) {
if _, err := exercice.WipeHints(); err != nil { if _, err := exercice.WipeHints(); err != nil {
errs = append(errs, err) errs = multierr.Append(errs, err)
} else { } else {
exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml") exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")
hints, berrs := buildExerciceHints(i, exercice, exceptions) hints, berrs := buildExerciceHints(i, exercice, exceptions)
errs = append(errs, berrs...) errs = multierr.Append(errs, berrs)
hintsBindings = map[int]*fic.EHint{} hintsBindings = map[int]*fic.EHint{}
for _, hint := range hints { for _, hint := range hints {
// Import hint // Import hint
if h, err := exercice.AddHint(hint.Hint.Title, hint.Hint.Content, hint.Hint.Cost); err != nil { if h, err := exercice.AddHint(hint.Hint.Title, hint.Hint.Content, hint.Hint.Cost); err != nil {
errs = append(errs, NewHintError(exercice, hint.Hint, hint.Line, err)) errs = multierr.Append(errs, NewHintError(exercice, hint.Hint, hint.Line, err))
} else { } else {
hintsBindings[hint.Line] = h hintsBindings[hint.Line] = h
@ -148,10 +149,10 @@ func SyncExerciceHints(i Importer, exercice *fic.Exercice, flagsBindings map[int
for _, nf := range hint.FlagsDeps { for _, nf := range hint.FlagsDeps {
if f, ok := flagsBindings[nf]; ok { if f, ok := flagsBindings[nf]; ok {
if herr := h.AddDepend(f); herr != nil { if herr := h.AddDepend(f); herr != nil {
errs = append(errs, NewHintError(exercice, hint.Hint, hint.Line, fmt.Errorf("error hint dependency to flag #%d: %w", nf, herr))) errs = multierr.Append(errs, NewHintError(exercice, hint.Hint, hint.Line, fmt.Errorf("error hint dependency to flag #%d: %w", nf, herr)))
} }
} else { } else {
errs = append(errs, NewHintError(exercice, hint.Hint, hint.Line, fmt.Errorf("error hint dependency to flag #%d: Unexistant flag", nf))) errs = multierr.Append(errs, NewHintError(exercice, hint.Hint, hint.Line, fmt.Errorf("error hint dependency to flag #%d: Unexistant flag", nf)))
} }
} }
} }

View File

@ -11,6 +11,7 @@ import (
"unicode" "unicode"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
) )
@ -35,11 +36,11 @@ func validatorRegexp(vre string) (validator_regexp *string) {
return return
} }
func getRawKey(input interface{}, validatorRe string, ordered bool, showLines bool, separator string) (raw string, prep string, errs []error) { func getRawKey(input interface{}, validatorRe string, ordered bool, showLines bool, separator string) (raw string, prep string, errs error) {
// Concatenate array // Concatenate array
if f, ok := input.([]interface{}); ok { if f, ok := input.([]interface{}); ok {
if len(validatorRe) > 0 { if len(validatorRe) > 0 {
errs = append(errs, fmt.Errorf("ValidatorRe cannot be defined for this kind of flag.")) errs = multierr.Append(errs, fmt.Errorf("ValidatorRe cannot be defined for this kind of flag."))
validatorRe = "" validatorRe = ""
} }
@ -47,20 +48,20 @@ func getRawKey(input interface{}, validatorRe string, ordered bool, showLines bo
separator = "," separator = ","
} else if len(separator) > 1 { } else if len(separator) > 1 {
separator = string(separator[0]) separator = string(separator[0])
errs = append(errs, fmt.Errorf("separator truncated to %q", separator)) errs = multierr.Append(errs, fmt.Errorf("separator truncated to %q", separator))
} }
var fitems []string var fitems []string
for i, v := range f { for i, v := range f {
if g, ok := v.(string); ok { if g, ok := v.(string); ok {
if strings.Index(g, separator) != -1 { if strings.Index(g, separator) != -1 {
errs = append(errs, fmt.Errorf("flag items cannot contain %q character as it is used as separator. Change the separator attribute for this flag.", separator)) errs = multierr.Append(errs, fmt.Errorf("flag items cannot contain %q character as it is used as separator. Change the separator attribute for this flag.", separator))
return return
} else { } else {
fitems = append(fitems, g) fitems = append(fitems, g)
} }
} else { } else {
errs = append(errs, fmt.Errorf("item %d has an invalid type: can only be string, is %T.", i, g)) errs = multierr.Append(errs, fmt.Errorf("item %d has an invalid type: can only be string, is %T.", i, g))
return return
} }
} }
@ -77,7 +78,7 @@ func getRawKey(input interface{}, validatorRe string, ordered bool, showLines bo
nbLines := 0 nbLines := 0
if showLines { if showLines {
if len(fitems) > 9 { if len(fitems) > 9 {
errs = append(errs, fmt.Errorf("too much items in vector to use ShowLines features, max 9.")) errs = multierr.Append(errs, fmt.Errorf("too much items in vector to use ShowLines features, max 9."))
} else { } else {
nbLines = len(fitems) nbLines = len(fitems)
} }
@ -91,7 +92,7 @@ func getRawKey(input interface{}, validatorRe string, ordered bool, showLines bo
} else if f, ok := input.(float64); ok { } else if f, ok := input.(float64); ok {
raw = strconv.FormatFloat(f, 'f', -1, 64) raw = strconv.FormatFloat(f, 'f', -1, 64)
} else if f, ok := input.(string); !ok { } else if f, ok := input.(string); !ok {
errs = append(errs, fmt.Errorf("has an invalid type: can only be []string or string, not %T", input)) errs = multierr.Append(errs, fmt.Errorf("has an invalid type: can only be []string or string, not %T", input))
return return
} else { } else {
raw = f raw = f
@ -99,21 +100,21 @@ func getRawKey(input interface{}, validatorRe string, ordered bool, showLines bo
return return
} }
func buildLabelFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, exceptions *CheckExceptions) (f *fic.FlagLabel, errs []error) { func buildLabelFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, exceptions *CheckExceptions) (f *fic.FlagLabel, errs error) {
if len(flag.Label) == 0 { if len(flag.Label) == 0 {
errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("Label cannot be empty."))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("Label cannot be empty.")))
return return
} }
// Call checks hooks // Call checks hooks
for _, h := range hooks.mdTextHooks { for _, h := range hooks.mdTextHooks {
for _, err := range h(flag.Label, exercice.Language, exceptions.Filter2ndCol(strconv.Itoa(flagline))) { for _, err := range multierr.Errors(h(flag.Label, exercice.Language, exceptions.Filter2ndCol(strconv.Itoa(flagline)))) {
errs = append(errs, NewFlagError(exercice, &flag, flagline, err)) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, err))
} }
} }
if mdlabel, err := ProcessMarkdown(GlobalImporter, flag.Label, exercice.Path); err != nil { if mdlabel, err := ProcessMarkdown(GlobalImporter, flag.Label, exercice.Path); err != nil {
errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("unable to parse property label as Markdown: %w", err))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("unable to parse property label as Markdown: %w", err)))
} else { } else {
if strings.Count(flag.Label, "\n\n") == 0 { if strings.Count(flag.Label, "\n\n") == 0 {
flag.Label = mdlabel[3 : len(mdlabel)-4] flag.Label = mdlabel[3 : len(mdlabel)-4]
@ -123,11 +124,11 @@ func buildLabelFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, exc
} }
if flag.Raw != nil { if flag.Raw != nil {
errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("raw cannot be defined."))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("raw cannot be defined.")))
} }
if len(flag.Choice) != 0 { if len(flag.Choice) != 0 {
errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("choices cannot be defined."))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("choices cannot be defined.")))
} }
f = &fic.FlagLabel{ f = &fic.FlagLabel{
@ -138,32 +139,33 @@ func buildLabelFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, exc
// Call checks hooks // Call checks hooks
for _, h := range hooks.flagLabelHooks { for _, h := range hooks.flagLabelHooks {
for _, e := range h(f, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline))) { for _, e := range multierr.Errors(h(f, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline)))) {
errs = append(errs, NewFlagError(exercice, &flag, flagline, e)) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, e))
} }
} }
return return
} }
func buildKeyFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, defaultLabel string, exceptions *CheckExceptions) (f *fic.Flag, choices []*fic.FlagChoice, errs []error) { func buildKeyFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, defaultLabel string, exceptions *CheckExceptions) (f *fic.Flag, choices []*fic.FlagChoice, errs error) {
if len(flag.Label) == 0 { if len(flag.Label) == 0 {
flag.Label = defaultLabel flag.Label = defaultLabel
} }
if len(flag.Variant) != 0 { if len(flag.Variant) != 0 {
errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("variant is not defined for this kind of flag."))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("variant is not defined for this kind of flag.")))
} }
if flag.Label[0] == '`' { if flag.Label[0] == '`' {
errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("Label should not begin with `."))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("Label should not begin with `.")))
flag.Label = flag.Label[1:] flag.Label = flag.Label[1:]
} }
raw, prep, terrs := getRawKey(flag.Raw, flag.CaptureRe, flag.Ordered, flag.ShowLines, flag.Separator) raw, prep, terrs := getRawKey(flag.Raw, flag.CaptureRe, flag.Ordered, flag.ShowLines, flag.Separator)
if len(terrs) > 0 { errors := multierr.Errors(terrs)
for _, terr := range terrs { if len(errors) > 0 {
errs = append(errs, NewFlagError(exercice, &flag, flagline, terr)) for _, terr := range errors {
errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, terr))
} }
f = nil f = nil
return return
@ -171,16 +173,16 @@ func buildKeyFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, defau
flag.Label = prep + flag.Label flag.Label = prep + flag.Label
if len(flag.Label) > 255 && flag.Type != "label" { if len(flag.Label) > 255 && flag.Type != "label" {
errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("label is too long (max 255 chars per label)."))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("label is too long (max 255 chars per label).")))
} }
if (flag.Type == "text" && !isFullGraphic(strings.Replace(raw, "\n", "", -1))) || (flag.Type != "text" && !isFullGraphic(raw)) { if (flag.Type == "text" && !isFullGraphic(strings.Replace(raw, "\n", "", -1))) || (flag.Type != "text" && !isFullGraphic(raw)) {
errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("WARNING non-printable characters in flag, is this really expected?"))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("WARNING non-printable characters in flag, is this really expected?")))
} }
hashedFlag, err := fic.ComputeHashedFlag([]byte(raw), !flag.CaseSensitive, flag.NoTrim, validatorRegexp(flag.CaptureRe), flag.SortReGroups) hashedFlag, err := fic.ComputeHashedFlag([]byte(raw), !flag.CaseSensitive, flag.NoTrim, validatorRegexp(flag.CaptureRe), flag.SortReGroups)
if err != nil { if err != nil {
errs = append(errs, NewFlagError(exercice, &flag, flagline, err)) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, err))
return return
} }
fk := &fic.FlagKey{ fk := &fic.FlagKey{
@ -202,8 +204,8 @@ func buildKeyFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, defau
// Call checks hooks // Call checks hooks
for _, h := range hooks.flagKeyHooks { for _, h := range hooks.flagKeyHooks {
for _, e := range h(fk, raw, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline))) { for _, e := range multierr.Errors(h(fk, raw, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline)))) {
errs = append(errs, NewFlagError(exercice, &flag, flagline, e)) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, e))
} }
} }
@ -222,9 +224,10 @@ func buildKeyFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, defau
for _, choice := range flag.Choice { for _, choice := range flag.Choice {
val, prep, terrs := getRawKey(choice.Value, "", false, false, "") val, prep, terrs := getRawKey(choice.Value, "", false, false, "")
if len(terrs) > 0 { errors := multierr.Errors(terrs)
for _, terr := range terrs { if len(errors) > 0 {
errs = append(errs, NewFlagError(exercice, &flag, flagline, terr)) for _, terr := range errors {
errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, terr))
} }
continue continue
} }
@ -245,15 +248,15 @@ func buildKeyFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, defau
// Call checks hooks // Call checks hooks
for _, h := range hooks.flagChoiceHooks { for _, h := range hooks.flagChoiceHooks {
for _, e := range h(fc, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline))) { for _, e := range multierr.Errors(h(fc, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline)))) {
errs = append(errs, NewFlagError(exercice, &flag, flagline, e)) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, e))
} }
} }
choices = append(choices, fc) choices = append(choices, fc)
if val == "true" || val == "false" { if val == "true" || val == "false" {
errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("value can't be %q, this is not a MCQ, the value has to be meaningful. The value is shown to players as response identifier.", val))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("value can't be %q, this is not a MCQ, the value has to be meaningful. The value is shown to players as response identifier.", val)))
} }
if val == raw || (!flag.CaseSensitive && val == strings.ToLower(raw)) { if val == raw || (!flag.CaseSensitive && val == strings.ToLower(raw)) {
@ -261,13 +264,13 @@ func buildKeyFlag(exercice *fic.Exercice, flag ExerciceFlag, flagline int, defau
} }
} }
if !hasOne { if !hasOne {
errs = append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("no valid answer defined."))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, fmt.Errorf("no valid answer defined.")))
} }
// Call checks hooks // Call checks hooks
for _, h := range hooks.flagKeyWithChoicesHooks { for _, h := range hooks.flagKeyWithChoicesHooks {
for _, e := range h(fk, raw, choices, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline))) { for _, e := range multierr.Errors(h(fk, raw, choices, exercice, exceptions.Filter2ndCol(strconv.Itoa(flagline)))) {
errs = append(errs, NewFlagError(exercice, &flag, flagline, e)) errs = multierr.Append(errs, NewFlagError(exercice, &flag, flagline, e))
} }
} }
} }
@ -297,7 +300,7 @@ func iface2Number(input interface{}, output *string) error {
} }
// buildExerciceFlag read challenge.txt and extract all flags. // buildExerciceFlag read challenge.txt and extract all flags.
func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nline int, exceptions *CheckExceptions) (ret []importFlag, errs []error) { func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nline int, exceptions *CheckExceptions) (ret []importFlag, errs error) {
switch strings.ToLower(flag.Type) { switch strings.ToLower(flag.Type) {
case "": case "":
flag.Type = "key" flag.Type = "key"
@ -309,17 +312,17 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
var smin, smax, sstep string var smin, smax, sstep string
err := iface2Number(flag.NumberMin, &smin) err := iface2Number(flag.NumberMin, &smin)
if err != nil { if err != nil {
errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("min %w", err))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("min %w", err)))
} }
err = iface2Number(flag.NumberMax, &smax) err = iface2Number(flag.NumberMax, &smax)
if err != nil { if err != nil {
errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("max %w", err))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("max %w", err)))
} }
err = iface2Number(flag.NumberStep, &sstep) err = iface2Number(flag.NumberStep, &sstep)
if err != nil { if err != nil {
errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("step %w", err))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("step %w", err)))
} }
flag.Type = fmt.Sprintf("number,%s,%s,%s", smin, smax, sstep) flag.Type = fmt.Sprintf("number,%s,%s,%s", smin, smax, sstep)
case "text": case "text":
@ -333,23 +336,23 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
case "mcq": case "mcq":
flag.Type = "mcq" flag.Type = "mcq"
default: default:
errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("invalid type of flag: should be 'key', 'number', 'text', 'mcq', 'ucq', 'radio' or 'vector'"))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("invalid type of flag: should be 'key', 'number', 'text', 'mcq', 'ucq', 'radio' or 'vector'")))
return return
} }
if !strings.HasPrefix(flag.Type, "number") { if !strings.HasPrefix(flag.Type, "number") {
if flag.NumberMin != nil { if flag.NumberMin != nil {
errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("property min undefined for this kind of flag: should the type be 'number'"))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("property min undefined for this kind of flag: should the type be 'number'")))
} else if flag.NumberMax != nil { } else if flag.NumberMax != nil {
errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("property max undefined for this kind of flag: should the type be 'number'"))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("property max undefined for this kind of flag: should the type be 'number'")))
} else if flag.NumberStep != nil { } else if flag.NumberStep != nil {
errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("property step undefined for this kind of flag: should the type be 'number'"))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("property step undefined for this kind of flag: should the type be 'number'")))
} }
} }
if len(flag.Help) > 0 { if len(flag.Help) > 0 {
if mdhelp, err := ProcessMarkdown(i, flag.Help, exercice.Path); err != nil { if mdhelp, err := ProcessMarkdown(i, flag.Help, exercice.Path); err != nil {
errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("unable to parse property help as Markdown: %w", err))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("unable to parse property help as Markdown: %w", err)))
} else { } else {
flag.Help = mdhelp[3 : len(mdhelp)-4] flag.Help = mdhelp[3 : len(mdhelp)-4]
} }
@ -357,7 +360,7 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
if flag.Type == "label" { if flag.Type == "label" {
addedFlag, berrs := buildLabelFlag(exercice, flag, nline+1, exceptions) addedFlag, berrs := buildLabelFlag(exercice, flag, nline+1, exceptions)
errs = append(errs, berrs...) errs = multierr.Append(errs, berrs)
if addedFlag != nil { if addedFlag != nil {
ret = append(ret, importFlag{ ret = append(ret, importFlag{
Line: nline + 1, Line: nline + 1,
@ -366,7 +369,7 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
} }
} else if flag.Type == "key" || strings.HasPrefix(flag.Type, "number") || flag.Type == "text" || flag.Type == "ucq" || flag.Type == "radio" || flag.Type == "vector" { } else if flag.Type == "key" || strings.HasPrefix(flag.Type, "number") || flag.Type == "text" || flag.Type == "ucq" || flag.Type == "radio" || flag.Type == "vector" {
addedFlag, choices, berrs := buildKeyFlag(exercice, flag, nline+1, "Flag", exceptions) addedFlag, choices, berrs := buildKeyFlag(exercice, flag, nline+1, "Flag", exceptions)
errs = append(errs, berrs...) errs = multierr.Append(errs, berrs)
if addedFlag != nil { if addedFlag != nil {
ret = append(ret, importFlag{ ret = append(ret, importFlag{
Line: nline + 1, Line: nline + 1,
@ -386,7 +389,7 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
isJustified := false isJustified := false
if len(flag.Variant) != 0 { if len(flag.Variant) != 0 {
errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("variant is not defined for this kind of flag"))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("variant is not defined for this kind of flag")))
} }
if !flag.NoShuffle { if !flag.NoShuffle {
@ -400,7 +403,7 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
if choice.Raw != nil { if choice.Raw != nil {
if hasOne && !isJustified { if hasOne && !isJustified {
errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("all true items has to be justified in this MCQ"))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("all true items has to be justified in this MCQ")))
continue continue
} }
@ -409,13 +412,13 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
} else if p, ok := choice.Value.(bool); ok { } else if p, ok := choice.Value.(bool); ok {
val = p val = p
if isJustified { if isJustified {
errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("all true items has to be justified in this MCQ"))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("all true items has to be justified in this MCQ")))
continue continue
} }
} else if choice.Value == nil { } else if choice.Value == nil {
val = false val = false
} else { } else {
errs = append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("choice %d: incorrect type for value: %T is not boolean.", cid, choice.Value))) errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, fmt.Errorf("choice %d: incorrect type for value: %T is not boolean.", cid, choice.Value)))
continue continue
} }
@ -427,9 +430,7 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
if isJustified && choice.Raw != nil { if isJustified && choice.Raw != nil {
addedFlag, choices, berrs := buildKeyFlag(exercice, choice.ExerciceFlag, nline+1, "Flag correspondant", exceptions) addedFlag, choices, berrs := buildKeyFlag(exercice, choice.ExerciceFlag, nline+1, "Flag correspondant", exceptions)
if len(berrs) > 0 { errs = multierr.Append(errs, berrs)
errs = append(errs, berrs...)
}
if addedFlag != nil { if addedFlag != nil {
ret = append(ret, importFlag{ ret = append(ret, importFlag{
Line: nline + 1, Line: nline + 1,
@ -443,8 +444,8 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
// Call checks hooks // Call checks hooks
for _, h := range hooks.flagMCQHooks { for _, h := range hooks.flagMCQHooks {
for _, e := range h(&addedFlag, addedFlag.Entries, exercice, exceptions) { for _, e := range multierr.Errors(h(&addedFlag, addedFlag.Entries, exercice, exceptions)) {
errs = append(errs, NewFlagError(exercice, &flag, nline+1, e)) errs = multierr.Append(errs, NewFlagError(exercice, &flag, nline+1, e))
} }
} }
@ -457,9 +458,9 @@ func buildExerciceFlag(i Importer, exercice *fic.Exercice, flag ExerciceFlag, nl
} }
// buildExerciceFlags read challenge.txt and extract all flags. // buildExerciceFlags read challenge.txt and extract all flags.
func buildExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (flags map[int64]importFlag, flagids []int64, errs []error) { func buildExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (flags map[int64]importFlag, flagids []int64, errs error) {
params, gerrs := getExerciceParams(i, exercice) params, gerrs := getExerciceParams(i, exercice)
if len(gerrs) > 0 { if len(multierr.Errors(gerrs)) > 0 {
return flags, flagids, gerrs return flags, flagids, gerrs
} }
@ -473,14 +474,12 @@ func buildExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExc
// Ensure flag ID is unique // Ensure flag ID is unique
for _, ok := flags[flag.Id]; ok; _, ok = flags[flag.Id] { for _, ok := flags[flag.Id]; ok; _, ok = flags[flag.Id] {
errs = append(errs, NewFlagError(exercice, &params.Flags[nline], nline+1, fmt.Errorf("identifier already used (%d), using a random one.", flag.Id))) errs = multierr.Append(errs, NewFlagError(exercice, &params.Flags[nline], nline+1, fmt.Errorf("identifier already used (%d), using a random one.", flag.Id)))
flag.Id = rand.Int63() flag.Id = rand.Int63()
} }
newFlags, ferrs := buildExerciceFlag(i, exercice, flag, nline, exceptions) newFlags, ferrs := buildExerciceFlag(i, exercice, flag, nline, exceptions)
if len(ferrs) > 0 { errs = multierr.Append(errs, ferrs)
errs = append(errs, ferrs...)
}
if len(newFlags) > 0 { if len(newFlags) > 0 {
for _, newFlag := range newFlags { for _, newFlag := range newFlags {
fId := flag.Id fId := flag.Id
@ -491,7 +490,7 @@ func buildExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExc
// Read dependency to flag // Read dependency to flag
for _, nf := range flag.NeedFlag { for _, nf := range flag.NeedFlag {
if len(nf.Theme) > 0 { if len(nf.Theme) > 0 {
errs = append(errs, NewFlagError(exercice, &params.Flags[nline], nline+1, fmt.Errorf("dependancy on another scenario is not implemented yet."))) errs = multierr.Append(errs, NewFlagError(exercice, &params.Flags[nline], nline+1, fmt.Errorf("dependancy on another scenario is not implemented yet.")))
} }
newFlag.FlagsDeps = append(newFlag.FlagsDeps, nf.Id) newFlag.FlagsDeps = append(newFlag.FlagsDeps, nf.Id)
} }
@ -514,18 +513,18 @@ func buildExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExc
} }
// CheckExerciceFlags checks if all flags for the given challenge are correct. // CheckExerciceFlags checks if all flags for the given challenge are correct.
func CheckExerciceFlags(i Importer, exercice *fic.Exercice, files []string, exceptions *CheckExceptions) (rf []fic.Flag, errs []error) { func CheckExerciceFlags(i Importer, exercice *fic.Exercice, files []string, exceptions *CheckExceptions) (rf []fic.Flag, errs error) {
exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml") exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")
flags, flagsids, berrs := buildExerciceFlags(i, exercice, exceptions) flags, flagsids, berrs := buildExerciceFlags(i, exercice, exceptions)
errs = append(errs, berrs...) errs = multierr.Append(errs, berrs)
for _, flagid := range flagsids { for _, flagid := range flagsids {
if flag, ok := flags[flagid]; ok { if flag, ok := flags[flagid]; ok {
// Check dependency to flag // Check dependency to flag
for _, nf := range flag.FlagsDeps { for _, nf := range flag.FlagsDeps {
if _, ok := flags[nf]; !ok { if _, ok := flags[nf]; !ok {
errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag depend on flag id=%d: id not defined", nf))) errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag depend on flag id=%d: id not defined", nf)))
} }
} }
@ -533,7 +532,7 @@ func CheckExerciceFlags(i Importer, exercice *fic.Exercice, files []string, exce
deps := flag.FlagsDeps deps := flag.FlagsDeps
for i := 0; i < len(deps); i++ { for i := 0; i < len(deps); i++ {
if deps[i] == flagid { if deps[i] == flagid {
errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag dependency loop detected: flag id=%d: depends on itself", flagid))) errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag dependency loop detected: flag id=%d: depends on itself", flagid)))
break break
} }
@ -558,7 +557,7 @@ func CheckExerciceFlags(i Importer, exercice *fic.Exercice, files []string, exce
} }
} }
if !found { if !found {
errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag depend on %s: No such file", lf))) errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("flag depend on %s: No such file", lf)))
} }
} }
@ -587,16 +586,16 @@ func ExerciceFlagsMap(i Importer, exercice *fic.Exercice) (kmap map[int64]fic.Fl
} }
// SyncExerciceFlags imports all kind of flags for the given challenge. // SyncExerciceFlags imports all kind of flags for the given challenge.
func SyncExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (kmap map[int64]fic.Flag, errs []error) { func SyncExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (kmap map[int64]fic.Flag, errs error) {
if _, err := exercice.WipeFlags(); err != nil { if _, err := exercice.WipeFlags(); err != nil {
errs = append(errs, err) errs = multierr.Append(errs, err)
} else if _, err := exercice.WipeMCQs(); err != nil { } else if _, err := exercice.WipeMCQs(); err != nil {
errs = append(errs, err) errs = multierr.Append(errs, err)
} else { } else {
exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml") exceptions = exceptions.GetFileExceptions("challenge.txt", "challenge.toml")
flags, flagids, berrs := buildExerciceFlags(i, exercice, exceptions) flags, flagids, berrs := buildExerciceFlags(i, exercice, exceptions)
errs = append(errs, berrs...) errs = multierr.Append(errs, berrs)
kmap = map[int64]fic.Flag{} kmap = map[int64]fic.Flag{}
@ -610,12 +609,12 @@ func SyncExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExce
} }
if addedFlag, err := exercice.AddFlag(flag.Flag); err != nil { if addedFlag, err := exercice.AddFlag(flag.Flag); err != nil {
errs = append(errs, NewFlagError(exercice, nil, flag.Line, err)) errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, err))
} else { } else {
if f, ok := addedFlag.(*fic.FlagKey); ok { if f, ok := addedFlag.(*fic.FlagKey); ok {
for _, choice := range flag.Choices { for _, choice := range flag.Choices {
if _, err := f.AddChoice(choice); err != nil { if _, err := f.AddChoice(choice); err != nil {
errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("choice #FIXME: %w", err))) errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("choice #FIXME: %w", err)))
} }
} }
} }
@ -625,18 +624,18 @@ func SyncExerciceFlags(i Importer, exercice *fic.Exercice, exceptions *CheckExce
// Import dependency to flag // Import dependency to flag
for _, nf := range flag.FlagsDeps { for _, nf := range flag.FlagsDeps {
if rf, ok := kmap[nf]; !ok { if rf, ok := kmap[nf]; !ok {
errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to flag id=%d: id not defined, perhaps not available at time of processing", nf))) errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to flag id=%d: id not defined, perhaps not available at time of processing", nf)))
} else if err := addedFlag.AddDepend(rf); err != nil { } else if err := addedFlag.AddDepend(rf); err != nil {
errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to id=%d: %w", nf, err))) errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to id=%d: %w", nf, err)))
} }
} }
// Import dependency to file // Import dependency to file
for _, lf := range flag.FilesDeps { for _, lf := range flag.FilesDeps {
if rf, err := exercice.GetFileByFilename(lf); err != nil { if rf, err := exercice.GetFileByFilename(lf); err != nil {
errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to %s: %w", lf, err))) errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to %s: %w", lf, err)))
} else if err := rf.AddDepend(addedFlag); err != nil { } else if err := rf.AddDepend(addedFlag); err != nil {
errs = append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to %s: %w", lf, err))) errs = multierr.Append(errs, NewFlagError(exercice, nil, flag.Line, fmt.Errorf("dependency to %s: %w", lf, err)))
} }
} }
} }

View File

@ -13,6 +13,7 @@ import (
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
) )
@ -93,7 +94,7 @@ func parseExerciceDirname(edir string) (eid int, ename string, err error) {
} }
// BuildExercice creates an Exercice from a given importer. // BuildExercice creates an Exercice from a given importer.
func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*fic.Exercice, exceptions_in *CheckExceptions) (e *fic.Exercice, p ExerciceParams, eid int, exceptions *CheckExceptions, edir string, errs []error) { func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*fic.Exercice, exceptions_in *CheckExceptions) (e *fic.Exercice, p ExerciceParams, eid int, exceptions *CheckExceptions, edir string, errs error) {
e = &fic.Exercice{} e = &fic.Exercice{}
e.Path = epath e.Path = epath
@ -104,7 +105,7 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
if err != nil { if err != nil {
// Ignore eid if we are certain this is an exercice directory, eid will be 0 // Ignore eid if we are certain this is an exercice directory, eid will be 0
if !i.Exists(path.Join(epath, "title.txt")) { if !i.Exists(path.Join(epath, "title.txt")) {
errs = append(errs, NewExerciceError(e, fmt.Errorf("unable to parse exercice directory: %w", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("unable to parse exercice directory: %w", err), theme))
return nil, p, eid, exceptions_in, edir, errs return nil, p, eid, exceptions_in, edir, errs
} }
} }
@ -118,7 +119,7 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
if language, err := GetFileContent(i, path.Join(epath, "language.txt")); err == nil { if language, err := GetFileContent(i, path.Join(epath, "language.txt")); err == nil {
language = strings.TrimSpace(language) language = strings.TrimSpace(language)
if strings.Contains(language, "\n") { if strings.Contains(language, "\n") {
errs = append(errs, NewExerciceError(e, fmt.Errorf("language.txt: Language can't contain new lines"), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("language.txt: Language can't contain new lines"), theme))
} else { } else {
e.Language = language e.Language = language
} }
@ -128,7 +129,7 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
if myTitle, err := GetFileContent(i, path.Join(epath, "title.txt")); err == nil { if myTitle, err := GetFileContent(i, path.Join(epath, "title.txt")); err == nil {
myTitle = strings.TrimSpace(myTitle) myTitle = strings.TrimSpace(myTitle)
if strings.Contains(myTitle, "\n") { if strings.Contains(myTitle, "\n") {
errs = append(errs, NewExerciceError(e, fmt.Errorf("title.txt: Title can't contain new lines"), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("title.txt: Title can't contain new lines"), theme))
} else { } else {
e.Title = myTitle e.Title = myTitle
} }
@ -136,7 +137,7 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
// Character reserved for WIP exercices // Character reserved for WIP exercices
if len(e.Title) > 0 && e.Title[0] == '%' { if len(e.Title) > 0 && e.Title[0] == '%' {
errs = append(errs, NewExerciceError(e, fmt.Errorf("title can't contain start by '%%'"), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("title can't contain start by '%%'"), theme))
} }
e.URLId = fic.ToURLid(e.Title) e.URLId = fic.ToURLid(e.Title)
@ -144,7 +145,7 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
if i.Exists(path.Join(epath, "AUTHORS.txt")) { if i.Exists(path.Join(epath, "AUTHORS.txt")) {
if authors, err := getAuthors(i, epath); err != nil { if authors, err := getAuthors(i, epath); err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("unable to get AUTHORS.txt: %w", err))) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("unable to get AUTHORS.txt: %w", err)))
} else { } else {
// Format authors // Format authors
e.Authors = strings.Join(authors, ", ") e.Authors = strings.Join(authors, ", ")
@ -158,13 +159,13 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
e.Headline, err = GetFileContent(i, path.Join(epath, "headline.md")) e.Headline, err = GetFileContent(i, path.Join(epath, "headline.md"))
} }
if err != nil { if err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("unable to get exercice's headline: %w", err))) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("unable to get exercice's headline: %w", err)))
} }
if e.Headline != "" { if e.Headline != "" {
// Call checks hooks // Call checks hooks
for _, h := range hooks.mdTextHooks { for _, h := range hooks.mdTextHooks {
for _, err := range h(e.Headline, e.Language, exceptions.GetFileExceptions("headline.md", "headline.txt")) { for _, err := range multierr.Errors(h(e.Headline, e.Language, exceptions.GetFileExceptions("headline.md", "headline.txt"))) {
errs = append(errs, NewExerciceError(e, fmt.Errorf("headline.md: %w", err))) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("headline.md: %w", err)))
} }
} }
} }
@ -178,14 +179,14 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
err = fmt.Errorf("Unable to find overview.txt nor overview.md") err = fmt.Errorf("Unable to find overview.txt nor overview.md")
} }
if err != nil { if err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("overview.txt: %s", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("overview.txt: %s", err), theme))
} else { } else {
e.Overview = fixnbsp(e.Overview) e.Overview = fixnbsp(e.Overview)
// Call checks hooks // Call checks hooks
for _, h := range hooks.mdTextHooks { for _, h := range hooks.mdTextHooks {
for _, err := range h(e.Overview, e.Language, exceptions.GetFileExceptions("overview.md", "overview.txt")) { for _, err := range multierr.Errors(h(e.Overview, e.Language, exceptions.GetFileExceptions("overview.md", "overview.txt"))) {
errs = append(errs, NewExerciceError(e, fmt.Errorf("overview.md: %w", err))) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("overview.md: %w", err)))
} }
} }
@ -193,14 +194,14 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
if e.Headline == "" { if e.Headline == "" {
err := goldmark.Convert([]byte(strings.Split(e.Overview, "\n")[0]), &buf) err := goldmark.Convert([]byte(strings.Split(e.Overview, "\n")[0]), &buf)
if err != nil { if err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("overview.md: an error occurs during markdown formating of the headline: %w", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("overview.md: an error occurs during markdown formating of the headline: %w", err), theme))
} else { } else {
e.Headline = string(buf.Bytes()) e.Headline = string(buf.Bytes())
} }
} }
if e.Overview, err = ProcessMarkdown(i, e.Overview, epath); err != nil { if e.Overview, err = ProcessMarkdown(i, e.Overview, epath); err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("overview.md: an error occurs during markdown formating: %w", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("overview.md: an error occurs during markdown formating: %w", err), theme))
} }
} }
@ -212,17 +213,17 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
err = fmt.Errorf("Unable to find statement.txt nor statement.md") err = fmt.Errorf("Unable to find statement.txt nor statement.md")
} }
if err != nil { if err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("statement.md: %w", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("statement.md: %w", err), theme))
} else { } else {
// Call checks hooks // Call checks hooks
for _, h := range hooks.mdTextHooks { for _, h := range hooks.mdTextHooks {
for _, err := range h(e.Statement, e.Language, exceptions.GetFileExceptions("statement.md", "statement.txt")) { for _, err := range multierr.Errors(h(e.Statement, e.Language, exceptions.GetFileExceptions("statement.md", "statement.txt"))) {
errs = append(errs, NewExerciceError(e, fmt.Errorf("statement.md: %w", err))) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("statement.md: %w", err)))
} }
} }
if e.Statement, err = ProcessMarkdown(i, fixnbsp(e.Statement), epath); err != nil { if e.Statement, err = ProcessMarkdown(i, fixnbsp(e.Statement), epath); err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("statement.md: an error occurs during markdown formating: %w", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("statement.md: an error occurs during markdown formating: %w", err), theme))
} }
} }
@ -232,17 +233,17 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
e.Finished, err = GetFileContent(i, path.Join(epath, "finished.md")) e.Finished, err = GetFileContent(i, path.Join(epath, "finished.md"))
} }
if err != nil { if err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("finished.md: %w", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("finished.md: %w", err), theme))
} else if len(e.Finished) > 0 { } else if len(e.Finished) > 0 {
// Call checks hooks // Call checks hooks
for _, h := range hooks.mdTextHooks { for _, h := range hooks.mdTextHooks {
for _, err := range h(e.Finished, e.Language, exceptions.GetFileExceptions("finished.md", "finished.txt")) { for _, err := range multierr.Errors(h(e.Finished, e.Language, exceptions.GetFileExceptions("finished.md", "finished.txt"))) {
errs = append(errs, NewExerciceError(e, fmt.Errorf("finished.md: %w", err))) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("finished.md: %w", err)))
} }
} }
if e.Finished, err = ProcessMarkdown(i, e.Finished, epath); err != nil { if e.Finished, err = ProcessMarkdown(i, e.Finished, epath); err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("finished.md: an error occurs during markdown formating: %w", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("finished.md: an error occurs during markdown formating: %w", err), theme))
} }
} }
@ -251,31 +252,31 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
} else if i.Exists(path.Join(epath, "heading.png")) { } else if i.Exists(path.Join(epath, "heading.png")) {
e.Image = path.Join(epath, "heading.png") e.Image = path.Join(epath, "heading.png")
} else if theme.Image == "" { } else if theme.Image == "" {
errs = append(errs, NewExerciceError(e, fmt.Errorf("heading.jpg: No such file"))) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("heading.jpg: No such file")))
} }
// Parse challenge.txt // Parse challenge.txt
var md toml.MetaData var md toml.MetaData
p, md, err = parseExerciceParams(i, epath) p, md, err = parseExerciceParams(i, epath)
if err != nil { if err != nil {
errs = append(errs, NewChallengeTxtError(e, 0, err, theme)) errs = multierr.Append(errs, NewChallengeTxtError(e, 0, err, theme))
return return
} }
// Alert about unknown keys in challenge.txt // Alert about unknown keys in challenge.txt
if len(md.Undecoded()) > 0 { if len(md.Undecoded()) > 0 {
for _, k := range md.Undecoded() { for _, k := range md.Undecoded() {
errs = append(errs, NewChallengeTxtError(e, 0, fmt.Errorf("unknown key %q found, check https://fic.srs.epita.fr/doc/files/challenge/", k), theme)) errs = multierr.Append(errs, NewChallengeTxtError(e, 0, fmt.Errorf("unknown key %q found, check https://fic.srs.epita.fr/doc/files/challenge/", k), theme))
} }
} }
e.WIP = p.WIP e.WIP = p.WIP
if p.WIP && !AllowWIPExercice { if p.WIP && !AllowWIPExercice {
errs = append(errs, NewExerciceError(e, fmt.Errorf("exercice declared Work In Progress in challenge.txt"), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("exercice declared Work In Progress in challenge.txt"), theme))
} }
if p.Gain == 0 { if p.Gain == 0 {
errs = append(errs, NewChallengeTxtError(e, 0, fmt.Errorf("Undefined gain for challenge"), theme)) errs = multierr.Append(errs, NewChallengeTxtError(e, 0, fmt.Errorf("Undefined gain for challenge"), theme))
} else { } else {
e.Gain = p.Gain e.Gain = p.Gain
} }
@ -283,11 +284,11 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
// Handle dependency // Handle dependency
if len(p.Dependencies) > 0 { if len(p.Dependencies) > 0 {
if len(p.Dependencies[0].Theme) > 0 && p.Dependencies[0].Theme != theme.Name { if len(p.Dependencies[0].Theme) > 0 && p.Dependencies[0].Theme != theme.Name {
errs = append(errs, NewExerciceError(e, fmt.Errorf("unable to treat dependency to another theme (%q): not implemented.", p.Dependencies[0].Theme), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("unable to treat dependency to another theme (%q): not implemented.", p.Dependencies[0].Theme), theme))
} else { } else {
if dmap == nil { if dmap == nil {
if dmap2, err := buildDependancyMap(i, theme); err != nil { if dmap2, err := buildDependancyMap(i, theme); err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("unable to build dependency map: %w", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("unable to build dependency map: %w", err), theme))
} else { } else {
dmap = &dmap2 dmap = &dmap2
} }
@ -305,7 +306,7 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
for k, _ := range *dmap { for k, _ := range *dmap {
dmap_keys = append(dmap_keys, fmt.Sprintf("%d", k)) dmap_keys = append(dmap_keys, fmt.Sprintf("%d", k))
} }
errs = append(errs, NewExerciceError(e, fmt.Errorf("Unable to find required exercice dependancy %d (available at time of processing: %s)", p.Dependencies[0].Id, strings.Join(dmap_keys, ",")), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("Unable to find required exercice dependancy %d (available at time of processing: %s)", p.Dependencies[0].Id, strings.Join(dmap_keys, ",")), theme))
} }
} }
} }
@ -318,10 +319,10 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
if !i.Exists(e.VideoURI) { if !i.Exists(e.VideoURI) {
e.VideoURI = "" e.VideoURI = ""
} else if size, err := GetFileSize(i, e.VideoURI); err != nil { } else if size, err := GetFileSize(i, e.VideoURI); err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("resolution.mp4: %w", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("resolution.mp4: %w", err), theme))
e.VideoURI = "" e.VideoURI = ""
} else if size == 0 { } else if size == 0 {
errs = append(errs, NewExerciceError(e, fmt.Errorf("resolution.mp4: The file is empty!"), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("resolution.mp4: The file is empty!"), theme))
e.VideoURI = "" e.VideoURI = ""
} else { } else {
e.VideoURI = strings.Replace(url.PathEscape(path.Join("$RFILES$", e.VideoURI)), "%2F", "/", -1) e.VideoURI = strings.Replace(url.PathEscape(path.Join("$RFILES$", e.VideoURI)), "%2F", "/", -1)
@ -335,21 +336,21 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
if i.Exists(writeup) { if i.Exists(writeup) {
if size, err := GetFileSize(i, writeup); err != nil { if size, err := GetFileSize(i, writeup); err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("resolution.md: %w", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("resolution.md: %w", err), theme))
} else if size == 0 { } else if size == 0 {
errs = append(errs, NewExerciceError(e, fmt.Errorf("resolution.md: The file is empty!"), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("resolution.md: The file is empty!"), theme))
} else if e.Resolution, err = GetFileContent(i, writeup); err != nil { } else if e.Resolution, err = GetFileContent(i, writeup); err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("resolution.md: %w", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("resolution.md: %w", err), theme))
} else { } else {
// Call checks hooks // Call checks hooks
for _, h := range hooks.mdTextHooks { for _, h := range hooks.mdTextHooks {
for _, err := range h(e.Resolution, e.Language, exceptions.GetFileExceptions("resolution.md"), p.GetRawFlags()...) { for _, err := range multierr.Errors(h(e.Resolution, e.Language, exceptions.GetFileExceptions("resolution.md"), p.GetRawFlags()...)) {
errs = append(errs, NewExerciceError(e, fmt.Errorf("resolution.md: %w", err))) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("resolution.md: %w", err)))
} }
} }
if e.Resolution, err = ProcessMarkdown(i, e.Resolution, epath); err != nil { if e.Resolution, err = ProcessMarkdown(i, e.Resolution, epath); err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("resolution.md: error during markdown processing: %w", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("resolution.md: error during markdown processing: %w", err), theme))
} else { } else {
resolutionFound = true resolutionFound = true
} }
@ -357,28 +358,26 @@ func BuildExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*
} }
if !resolutionFound { if !resolutionFound {
errs = append(errs, NewExerciceError(e, ErrResolutionNotFound, theme)) errs = multierr.Append(errs, NewExerciceError(e, ErrResolutionNotFound, theme))
} }
// Call checks hooks // Call checks hooks
for _, h := range hooks.exerciceHooks { for _, h := range hooks.exerciceHooks {
for _, err := range h(e, exceptions) { for _, err := range multierr.Errors(h(e, exceptions)) {
errs = append(errs, NewExerciceError(e, err)) errs = multierr.Append(errs, NewExerciceError(e, err))
} }
} }
return return
} }
// SyncExercice imports new or updates existing given exercice. // SyncExercice imports new or updates existing given exercice.
func SyncExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*fic.Exercice, exceptions_in *CheckExceptions) (e *fic.Exercice, eid int, exceptions *CheckExceptions, errs []error) { func SyncExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*fic.Exercice, exceptions_in *CheckExceptions) (e *fic.Exercice, eid int, exceptions *CheckExceptions, errs error) {
var err error var err error
var p ExerciceParams var p ExerciceParams
var berrors []error var berrors error
e, p, eid, exceptions, _, berrors = BuildExercice(i, theme, epath, dmap, exceptions_in) e, p, eid, exceptions, _, berrors = BuildExercice(i, theme, epath, dmap, exceptions_in)
for _, e := range berrors { errs = multierr.Append(errs, berrors)
errs = append(errs, e)
}
if e != nil { if e != nil {
if len(e.Image) > 0 { if len(e.Image) > 0 {
@ -396,24 +395,24 @@ func SyncExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*f
return nil, err return nil, err
}); err != nil { }); err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("unable to import heading image: %w", err))) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("unable to import heading image: %w", err)))
} }
} }
// Create or update the exercice // Create or update the exercice
err = theme.SaveNamedExercice(e) err = theme.SaveNamedExercice(e)
if err != nil { if err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("error on exercice save: %w", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("error on exercice save: %w", err), theme))
return return
} }
// Import eercice tags // Import eercice tags
if _, err := e.WipeTags(); err != nil { if _, err := e.WipeTags(); err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("unable to wipe tags: %w", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("unable to wipe tags: %w", err), theme))
} }
for _, tag := range p.Tags { for _, tag := range p.Tags {
if _, err := e.AddTag(tag); err != nil { if _, err := e.AddTag(tag); err != nil {
errs = append(errs, NewExerciceError(e, fmt.Errorf("unable to add tag: %w", err), theme)) errs = multierr.Append(errs, NewExerciceError(e, fmt.Errorf("unable to add tag: %w", err), theme))
return return
} }
} }
@ -423,9 +422,9 @@ func SyncExercice(i Importer, theme *fic.Theme, epath string, dmap *map[int64]*f
} }
// SyncExercices imports new or updates existing exercices, in a given theme. // SyncExercices imports new or updates existing exercices, in a given theme.
func SyncExercices(i Importer, theme *fic.Theme, exceptions *CheckExceptions) (exceptions_out map[int]*CheckExceptions, errs []error) { func SyncExercices(i Importer, theme *fic.Theme, exceptions *CheckExceptions) (exceptions_out map[int]*CheckExceptions, errs error) {
if exercices, err := GetExercices(i, theme); err != nil { if exercices, err := GetExercices(i, theme); err != nil {
errs = append(errs, err) errs = multierr.Append(errs, err)
} else { } else {
exceptions_out = make(map[int]*CheckExceptions) exceptions_out = make(map[int]*CheckExceptions)
emap := map[string]int{} emap := map[string]int{}
@ -438,7 +437,7 @@ func SyncExercices(i Importer, theme *fic.Theme, exceptions *CheckExceptions) (e
emap[e.Title] = eid emap[e.Title] = eid
dmap[int64(eid)] = e dmap[int64(eid)] = e
exceptions_out[eid] = ex_exceptions exceptions_out[eid] = ex_exceptions
errs = append(errs, cur_errs...) errs = multierr.Append(errs, cur_errs)
} }
} }

View File

@ -8,6 +8,8 @@ import (
"sync" "sync"
"time" "time"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/admin/generation" "srs.epita.fr/fic-server/admin/generation"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
) )
@ -49,7 +51,7 @@ func SpeedySyncDeep(i Importer) (errs SyncReport) {
errs.DateStart = startTime errs.DateStart = startTime
exceptions, sterrs := SyncThemes(i) exceptions, sterrs := SyncThemes(i)
for _, sterr := range sterrs { for _, sterr := range multierr.Errors(sterrs) {
errs.ThemesSync = append(errs.ThemesSync, sterr.Error()) errs.ThemesSync = append(errs.ThemesSync, sterr.Error())
} }
@ -60,7 +62,7 @@ func SpeedySyncDeep(i Importer) (errs SyncReport) {
for tid, theme := range themes { for tid, theme := range themes {
DeepSyncProgress = 3 + uint8(tid)*themeStep DeepSyncProgress = 3 + uint8(tid)*themeStep
ex_exceptions, seerrs := SyncExercices(i, theme, exceptions[theme.Path]) ex_exceptions, seerrs := SyncExercices(i, theme, exceptions[theme.Path])
for _, seerr := range seerrs { for _, seerr := range multierr.Errors(seerrs) {
errs.Themes[theme.Name] = append(errs.Themes[theme.Name], seerr.Error()) errs.Themes[theme.Name] = append(errs.Themes[theme.Name], seerr.Error())
} }
@ -74,13 +76,13 @@ func SpeedySyncDeep(i Importer) (errs SyncReport) {
DeepSyncProgress = 3 + uint8(tid)*themeStep + uint8(eid)*exerciceStep DeepSyncProgress = 3 + uint8(tid)*themeStep + uint8(eid)*exerciceStep
flagsBindings, ferrs := SyncExerciceFlags(i, exercice, ex_exceptions[eid]) flagsBindings, ferrs := SyncExerciceFlags(i, exercice, ex_exceptions[eid])
for _, ferr := range ferrs { for _, ferr := range multierr.Errors(ferrs) {
errs.Themes[theme.Name] = append(errs.Themes[theme.Name], ferr.Error()) errs.Themes[theme.Name] = append(errs.Themes[theme.Name], ferr.Error())
} }
DeepSyncProgress += exerciceStep / 2 DeepSyncProgress += exerciceStep / 2
_, herrs := SyncExerciceHints(i, exercice, flagsBindings, ex_exceptions[eid]) _, herrs := SyncExerciceHints(i, exercice, flagsBindings, ex_exceptions[eid])
for _, herr := range herrs { for _, herr := range multierr.Errors(herrs) {
errs.Themes[theme.Name] = append(errs.Themes[theme.Name], herr.Error()) errs.Themes[theme.Name] = append(errs.Themes[theme.Name], herr.Error())
} }
} }
@ -113,7 +115,7 @@ func SyncDeep(i Importer) (errs SyncReport) {
errs.DateStart = startTime errs.DateStart = startTime
exceptions, sterrs := SyncThemes(i) exceptions, sterrs := SyncThemes(i)
for _, sterr := range sterrs { for _, sterr := range multierr.Errors(sterrs) {
errs.ThemesSync = append(errs.ThemesSync, sterr.Error()) errs.ThemesSync = append(errs.ThemesSync, sterr.Error())
} }
@ -123,7 +125,7 @@ func SyncDeep(i Importer) (errs SyncReport) {
for tid, theme := range themes { for tid, theme := range themes {
stderrs := SyncThemeDeep(i, theme, tid, themeStep, exceptions[theme.Path]) stderrs := SyncThemeDeep(i, theme, tid, themeStep, exceptions[theme.Path])
for _, stderr := range stderrs { for _, stderr := range multierr.Errors(stderrs) {
errs.Themes[theme.Name] = append(errs.Themes[theme.Name], stderr.Error()) errs.Themes[theme.Name] = append(errs.Themes[theme.Name], stderr.Error())
} }
} }
@ -199,7 +201,7 @@ func EditDeepReport(errs *SyncReport, erase bool) {
} }
// SyncThemeDeep performs a recursive synchronisation: from challenges to challenge items. // SyncThemeDeep performs a recursive synchronisation: from challenges to challenge items.
func SyncThemeDeep(i Importer, theme *fic.Theme, tid int, themeStep uint8, exceptions *CheckExceptions) (errs []error) { func SyncThemeDeep(i Importer, theme *fic.Theme, tid int, themeStep uint8, exceptions *CheckExceptions) (errs error) {
var ex_exceptions map[int]*CheckExceptions var ex_exceptions map[int]*CheckExceptions
oneThemeDeepSync.Lock() oneThemeDeepSync.Lock()
@ -214,15 +216,15 @@ func SyncThemeDeep(i Importer, theme *fic.Theme, tid int, themeStep uint8, excep
log.Printf("Deep synchronization in progress: %d/255 - doing Theme %q, Exercice %q: %q\n", DeepSyncProgress, theme.Name, exercice.Title, exercice.Path) log.Printf("Deep synchronization in progress: %d/255 - doing Theme %q, Exercice %q: %q\n", DeepSyncProgress, theme.Name, exercice.Title, exercice.Path)
DeepSyncProgress = 3 + uint8(tid)*themeStep + uint8(eid)*exerciceStep DeepSyncProgress = 3 + uint8(tid)*themeStep + uint8(eid)*exerciceStep
errs = append(errs, SyncExerciceFiles(i, exercice, ex_exceptions[eid])...) errs = multierr.Append(errs, SyncExerciceFiles(i, exercice, ex_exceptions[eid]))
DeepSyncProgress += exerciceStep / 3 DeepSyncProgress += exerciceStep / 3
flagsBindings, ferrs := SyncExerciceFlags(i, exercice, ex_exceptions[eid]) flagsBindings, ferrs := SyncExerciceFlags(i, exercice, ex_exceptions[eid])
errs = append(errs, ferrs...) errs = multierr.Append(errs, ferrs)
DeepSyncProgress += exerciceStep / 3 DeepSyncProgress += exerciceStep / 3
_, herrs := SyncExerciceHints(i, exercice, flagsBindings, ex_exceptions[eid]) _, herrs := SyncExerciceHints(i, exercice, flagsBindings, ex_exceptions[eid])
errs = append(errs, herrs...) errs = multierr.Append(errs, herrs)
} }
} }

View File

@ -9,16 +9,16 @@ import (
var hooks = &CheckHooks{customHooks: map[string]CustomCheckHook{}} var hooks = &CheckHooks{customHooks: map[string]CustomCheckHook{}}
type CheckFlagChoiceHook func(*fic.FlagChoice, *fic.Exercice, *CheckExceptions) []error type CheckFlagChoiceHook func(*fic.FlagChoice, *fic.Exercice, *CheckExceptions) error
type CheckFlagKeyHook func(*fic.FlagKey, string, *fic.Exercice, *CheckExceptions) []error type CheckFlagKeyHook func(*fic.FlagKey, string, *fic.Exercice, *CheckExceptions) error
type CheckFlagKeyWithChoicesHook func(*fic.FlagKey, string, []*fic.FlagChoice, *fic.Exercice, *CheckExceptions) []error type CheckFlagKeyWithChoicesHook func(*fic.FlagKey, string, []*fic.FlagChoice, *fic.Exercice, *CheckExceptions) error
type CheckFlagLabelHook func(*fic.FlagLabel, *fic.Exercice, *CheckExceptions) []error type CheckFlagLabelHook func(*fic.FlagLabel, *fic.Exercice, *CheckExceptions) error
type CheckFlagMCQHook func(*fic.MCQ, []*fic.MCQ_entry, *fic.Exercice, *CheckExceptions) []error type CheckFlagMCQHook func(*fic.MCQ, []*fic.MCQ_entry, *fic.Exercice, *CheckExceptions) error
type CheckFileHook func(*fic.EFile, *fic.Exercice, *CheckExceptions) []error type CheckFileHook func(*fic.EFile, *fic.Exercice, *CheckExceptions) error
type CheckHintHook func(*fic.EHint, *fic.Exercice, *CheckExceptions) []error type CheckHintHook func(*fic.EHint, *fic.Exercice, *CheckExceptions) error
type CheckMDTextHook func(string, string, *CheckExceptions, ...string) []error type CheckMDTextHook func(string, string, *CheckExceptions, ...string) error
type CheckExerciceHook func(*fic.Exercice, *CheckExceptions) []error type CheckExerciceHook func(*fic.Exercice, *CheckExceptions) error
type CustomCheckHook func(interface{}, *CheckExceptions) []error type CustomCheckHook func(interface{}, *CheckExceptions) error
type CheckHooks struct { type CheckHooks struct {
flagChoiceHooks []CheckFlagChoiceHook flagChoiceHooks []CheckFlagChoiceHook
@ -73,7 +73,7 @@ func (h *CheckHooks) RegisterCustomHook(hookname string, f CustomCheckHook) {
h.customHooks[hookname] = f h.customHooks[hookname] = f
} }
func (h *CheckHooks) CallCustomHook(hookname string, data interface{}, exceptions *CheckExceptions) []error { func (h *CheckHooks) CallCustomHook(hookname string, data interface{}, exceptions *CheckExceptions) error {
if v, ok := h.customHooks[hookname]; ok { if v, ok := h.customHooks[hookname]; ok {
return v(data, exceptions) return v(data, exceptions)
} }

View File

@ -15,6 +15,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
"go.uber.org/multierr"
"golang.org/x/image/draw" "golang.org/x/image/draw"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
@ -104,7 +105,7 @@ func getAuthors(i Importer, tname string) ([]string, error) {
} }
// BuildTheme creates a Theme from a given importer. // BuildTheme creates a Theme from a given importer.
func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExceptions, errs []error) { func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExceptions, errs error) {
th = &fic.Theme{} th = &fic.Theme{}
th.Path = tdir th.Path = tdir
@ -116,7 +117,7 @@ func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExcept
if language, err := GetFileContent(i, path.Join(tdir, "language.txt")); err == nil { if language, err := GetFileContent(i, path.Join(tdir, "language.txt")); err == nil {
language = strings.TrimSpace(language) language = strings.TrimSpace(language)
if strings.Contains(language, "\n") { if strings.Contains(language, "\n") {
errs = append(errs, NewThemeError(th, fmt.Errorf("language.txt: Language can't contain new lines"))) errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("language.txt: Language can't contain new lines")))
} else { } else {
th.Language = language th.Language = language
} }
@ -133,7 +134,7 @@ func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExcept
th.URLId = fic.ToURLid(th.Name) th.URLId = fic.ToURLid(th.Name)
if authors, err := getAuthors(i, tdir); err != nil { if authors, err := getAuthors(i, tdir); err != nil {
errs = append(errs, NewThemeError(th, fmt.Errorf("unable to get AUTHORS.txt: %w", err))) errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("unable to get AUTHORS.txt: %w", err)))
return nil, nil, errs return nil, nil, errs
} else { } else {
// Format authors // Format authors
@ -147,13 +148,13 @@ func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExcept
th.Headline, err = GetFileContent(i, path.Join(tdir, "headline.md")) th.Headline, err = GetFileContent(i, path.Join(tdir, "headline.md"))
} }
if err != nil { if err != nil {
errs = append(errs, NewThemeError(th, fmt.Errorf("unable to get theme's headline: %w", err))) errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("unable to get theme's headline: %w", err)))
} }
if th.Headline != "" { if th.Headline != "" {
// Call checks hooks // Call checks hooks
for _, h := range hooks.mdTextHooks { for _, h := range hooks.mdTextHooks {
for _, err := range h(th.Headline, th.Language, exceptions.GetFileExceptions("headline.md", "headline.txt")) { for _, err := range multierr.Errors(h(th.Headline, th.Language, exceptions.GetFileExceptions("headline.md", "headline.txt"))) {
errs = append(errs, NewThemeError(th, fmt.Errorf("headline.md: %w", err))) errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("headline.md: %w", err)))
} }
} }
} }
@ -167,12 +168,12 @@ func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExcept
err = fmt.Errorf("unable to find overview.txt nor overview.md") err = fmt.Errorf("unable to find overview.txt nor overview.md")
} }
if err != nil { if err != nil {
errs = append(errs, NewThemeError(th, fmt.Errorf("unable to get theme's overview: %w", err))) errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("unable to get theme's overview: %w", err)))
} else { } else {
// Call checks hooks // Call checks hooks
for _, h := range hooks.mdTextHooks { for _, h := range hooks.mdTextHooks {
for _, err := range h(intro, th.Language, exceptions.GetFileExceptions("overview.md", "overview.txt")) { for _, err := range multierr.Errors(h(intro, th.Language, exceptions.GetFileExceptions("overview.md", "overview.txt"))) {
errs = append(errs, NewThemeError(th, fmt.Errorf("overview.md: %w", err))) errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("overview.md: %w", err)))
} }
} }
@ -188,12 +189,12 @@ func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExcept
// Format overview (markdown) // Format overview (markdown)
th.Intro, err = ProcessMarkdown(i, intro, tdir) th.Intro, err = ProcessMarkdown(i, intro, tdir)
if err != nil { if err != nil {
errs = append(errs, NewThemeError(th, fmt.Errorf("overview.txt: an error occurs during markdown formating: %w", err))) errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("overview.txt: an error occurs during markdown formating: %w", err)))
} }
var buf bytes.Buffer var buf bytes.Buffer
err := goldmark.Convert([]byte(th.Headline), &buf) err := goldmark.Convert([]byte(th.Headline), &buf)
if err != nil { if err != nil {
errs = append(errs, NewThemeError(th, fmt.Errorf("overview.txt: an error occurs during markdown formating of the headline: %w", err))) errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("overview.txt: an error occurs during markdown formating of the headline: %w", err)))
} else { } else {
th.Headline = string(buf.Bytes()) th.Headline = string(buf.Bytes())
} }
@ -204,7 +205,7 @@ func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExcept
} else if i.Exists(path.Join(tdir, "heading.png")) { } else if i.Exists(path.Join(tdir, "heading.png")) {
th.Image = path.Join(tdir, "heading.png") th.Image = path.Join(tdir, "heading.png")
} else { } else {
errs = append(errs, NewThemeError(th, fmt.Errorf("heading.jpg: No such file"))) errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("heading.jpg: No such file")))
} }
if i.Exists(path.Join(tdir, "partner.jpg")) { if i.Exists(path.Join(tdir, "partner.jpg")) {
@ -215,11 +216,11 @@ func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExcept
if i.Exists(path.Join(tdir, "partner.txt")) { if i.Exists(path.Join(tdir, "partner.txt")) {
if txt, err := GetFileContent(i, path.Join(tdir, "partner.txt")); err != nil { if txt, err := GetFileContent(i, path.Join(tdir, "partner.txt")); err != nil {
errs = append(errs, NewThemeError(th, fmt.Errorf("unable to get partner's text: %w", err))) errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("unable to get partner's text: %w", err)))
} else { } else {
th.PartnerText, err = ProcessMarkdown(i, txt, tdir) th.PartnerText, err = ProcessMarkdown(i, txt, tdir)
if err != nil { if err != nil {
errs = append(errs, NewThemeError(th, fmt.Errorf("partner.txt: an error occurs during markdown formating: %w", err))) errs = multierr.Append(errs, NewThemeError(th, fmt.Errorf("partner.txt: an error occurs during markdown formating: %w", err)))
} }
} }
} }
@ -227,9 +228,9 @@ func BuildTheme(i Importer, tdir string) (th *fic.Theme, exceptions *CheckExcept
} }
// SyncThemes imports new or updates existing themes. // SyncThemes imports new or updates existing themes.
func SyncThemes(i Importer) (exceptions map[string]*CheckExceptions, errs []error) { func SyncThemes(i Importer) (exceptions map[string]*CheckExceptions, errs error) {
if themes, err := GetThemes(i); err != nil { if themes, err := GetThemes(i); err != nil {
errs = append(errs, fmt.Errorf("Unable to list themes: %w", err)) errs = multierr.Append(errs, fmt.Errorf("Unable to list themes: %w", err))
} else { } else {
rand.Shuffle(len(themes), func(i, j int) { rand.Shuffle(len(themes), func(i, j int) {
themes[i], themes[j] = themes[j], themes[i] themes[i], themes[j] = themes[j], themes[i]
@ -239,9 +240,7 @@ func SyncThemes(i Importer) (exceptions map[string]*CheckExceptions, errs []erro
for _, tdir := range themes { for _, tdir := range themes {
btheme, excepts, berrs := BuildTheme(i, tdir) btheme, excepts, berrs := BuildTheme(i, tdir)
for _, e := range berrs { errs = multierr.Append(errs, berrs)
errs = append(errs, e)
}
if btheme == nil { if btheme == nil {
continue continue
@ -259,7 +258,7 @@ func SyncThemes(i Importer) (exceptions map[string]*CheckExceptions, errs []erro
btheme.Image = strings.TrimPrefix(filePath, fic.FilesDir) btheme.Image = strings.TrimPrefix(filePath, fic.FilesDir)
return nil, nil return nil, nil
}); err != nil { }); err != nil {
errs = append(errs, NewThemeError(btheme, fmt.Errorf("unable to import heading image: %w", err))) errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("unable to import heading image: %w", err)))
} }
} }
@ -269,14 +268,14 @@ func SyncThemes(i Importer) (exceptions map[string]*CheckExceptions, errs []erro
btheme.PartnerImage = strings.TrimPrefix(filePath, fic.FilesDir) btheme.PartnerImage = strings.TrimPrefix(filePath, fic.FilesDir)
return nil, nil return nil, nil
}); err != nil { }); err != nil {
errs = append(errs, NewThemeError(btheme, fmt.Errorf("unable to import partner image: %w", err))) errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("unable to import partner image: %w", err)))
} }
} }
var theme *fic.Theme var theme *fic.Theme
if theme, err = fic.GetThemeByPath(btheme.Path); err != nil { if theme, err = fic.GetThemeByPath(btheme.Path); err != nil {
if _, err := fic.CreateTheme(btheme); err != nil { if _, err := fic.CreateTheme(btheme); err != nil {
errs = append(errs, NewThemeError(btheme, fmt.Errorf("an error occurs during add: %w", err))) errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("an error occurs during add: %w", err)))
continue continue
} }
} }
@ -284,7 +283,7 @@ func SyncThemes(i Importer) (exceptions map[string]*CheckExceptions, errs []erro
if !fic.CmpTheme(theme, btheme) { if !fic.CmpTheme(theme, btheme) {
btheme.Id = theme.Id btheme.Id = theme.Id
if _, err := btheme.Update(); err != nil { if _, err := btheme.Update(); err != nil {
errs = append(errs, NewThemeError(btheme, fmt.Errorf("an error occurs during update: %w", err))) errs = multierr.Append(errs, NewThemeError(btheme, fmt.Errorf("an error occurs during update: %w", err)))
continue continue
} }
} }

1
go.mod
View File

@ -14,6 +14,7 @@ require (
github.com/u2takey/ffmpeg-go v0.5.0 github.com/u2takey/ffmpeg-go v0.5.0
github.com/yuin/goldmark v1.6.0 github.com/yuin/goldmark v1.6.0
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b
go.uber.org/multierr v1.11.0
golang.org/x/crypto v0.15.0 golang.org/x/crypto v0.15.0
golang.org/x/image v0.14.0 golang.org/x/image v0.14.0
golang.org/x/oauth2 v0.14.0 golang.org/x/oauth2 v0.14.0

2
go.sum
View File

@ -345,6 +345,8 @@ github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b h1:7gd+rd8P3bqcn/96gOZa3F5dpJr/vEiDQYlNb/y2uNs=
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE= gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs= gocv.io/x/gocv v0.25.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=

View File

@ -5,29 +5,31 @@ import (
"path" "path"
"strings" "strings"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
) )
func EPITACheckFile(file *fic.EFile, _ *fic.Exercice, exceptions *sync.CheckExceptions) (errs []error) { func EPITACheckFile(file *fic.EFile, _ *fic.Exercice, exceptions *sync.CheckExceptions) (errs error) {
// Enforce file format // Enforce file format
if path.Ext(file.Name) == ".rar" || path.Ext(file.Name) == ".7z" { if path.Ext(file.Name) == ".rar" || path.Ext(file.Name) == ".7z" {
errs = append(errs, fmt.Errorf("this file use a forbidden archive type.")) errs = multierr.Append(errs, fmt.Errorf("this file use a forbidden archive type."))
} }
// Check for stange file extension // Check for stange file extension
if strings.HasSuffix(file.Name, ".tar.zip") { if strings.HasSuffix(file.Name, ".tar.zip") {
errs = append(errs, fmt.Errorf(".tar.zip is not a valid tar format")) errs = multierr.Append(errs, fmt.Errorf(".tar.zip is not a valid tar format"))
} }
// Check .gz files have a dedicated hash // Check .gz files have a dedicated hash
if path.Ext(file.Name) == ".gz" && !strings.HasSuffix(file.Name, ".tar.gz") && len(file.ChecksumShown) == 0 { if path.Ext(file.Name) == ".gz" && !strings.HasSuffix(file.Name, ".tar.gz") && len(file.ChecksumShown) == 0 {
errs = append(errs, fmt.Errorf("digest of original, uncompressed, file missing in DIGESTS.txt")) errs = multierr.Append(errs, fmt.Errorf("digest of original, uncompressed, file missing in DIGESTS.txt"))
} }
// Check for huge file to compress // Check for huge file to compress
if file.Size > 4000000 && path.Ext(file.Name) == ".tar" { if file.Size > 4000000 && path.Ext(file.Name) == ".tar" {
errs = append(errs, fmt.Errorf("archive to compress with bzip2")) errs = multierr.Append(errs, fmt.Errorf("archive to compress with bzip2"))
} else if file.Size > 40000000 && (path.Ext(file.Name) == "" || } else if file.Size > 40000000 && (path.Ext(file.Name) == "" ||
path.Ext(file.Name) == ".csv" || path.Ext(file.Name) == ".csv" ||
path.Ext(file.Name) == ".dump" || path.Ext(file.Name) == ".dump" ||
@ -38,7 +40,7 @@ func EPITACheckFile(file *fic.EFile, _ *fic.Exercice, exceptions *sync.CheckExce
path.Ext(file.Name) == ".pcap" || path.Ext(file.Name) == ".pcap" ||
path.Ext(file.Name) == ".pcapng" || path.Ext(file.Name) == ".pcapng" ||
path.Ext(file.Name) == ".txt") { path.Ext(file.Name) == ".txt") {
errs = append(errs, fmt.Errorf("huge file to compress with gzip")) errs = multierr.Append(errs, fmt.Errorf("huge file to compress with gzip"))
} }
return return

View File

@ -6,53 +6,55 @@ import (
"strings" "strings"
"unicode" "unicode"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
) )
func EPITACheckKeyFlag(flag *fic.FlagKey, raw string, _ *fic.Exercice, exceptions *sync.CheckExceptions) (errs []error) { func EPITACheckKeyFlag(flag *fic.FlagKey, raw string, _ *fic.Exercice, exceptions *sync.CheckExceptions) (errs error) {
if (flag.Label[0] == 'Q' || flag.Label[0] == 'q') && (flag.Label[1] == 'U' || flag.Label[1] == 'u') || if (flag.Label[0] == 'Q' || flag.Label[0] == 'q') && (flag.Label[1] == 'U' || flag.Label[1] == 'u') ||
(flag.Label[0] == 'W' || flag.Label[0] == 'w') && (flag.Label[1] == 'H' || flag.Label[1] == 'h') { (flag.Label[0] == 'W' || flag.Label[0] == 'w') && (flag.Label[1] == 'H' || flag.Label[1] == 'h') {
errs = append(errs, fmt.Errorf("Label should not begin with %s. This seem to be a question. Reword your label as a description of the expected flag, `:` are automatically appended.", flag.Label[0:2])) errs = multierr.Append(errs, fmt.Errorf("Label should not begin with %s. This seem to be a question. Reword your label as a description of the expected flag, `:` are automatically appended.", flag.Label[0:2]))
flag.Label = flag.Label[1:] flag.Label = flag.Label[1:]
} }
if flag.Label[len(flag.Label)-1] != ')' && flag.Label[len(flag.Label)-1] != '©' && !unicode.IsLetter(rune(flag.Label[len(flag.Label)-1])) && !unicode.IsDigit(rune(flag.Label[len(flag.Label)-1])) { if flag.Label[len(flag.Label)-1] != ')' && flag.Label[len(flag.Label)-1] != '©' && !unicode.IsLetter(rune(flag.Label[len(flag.Label)-1])) && !unicode.IsDigit(rune(flag.Label[len(flag.Label)-1])) {
errs = append(errs, fmt.Errorf("Label should not end with punct (%q). Reword your label as a description of the expected flag, `:` are automatically appended.", flag.Label[len(flag.Label)-1])) errs = multierr.Append(errs, fmt.Errorf("Label should not end with punct (%q). Reword your label as a description of the expected flag, `:` are automatically appended.", flag.Label[len(flag.Label)-1]))
} }
if strings.HasPrefix(strings.ToLower(raw), "cve-") && flag.Type != "ucq" { if strings.HasPrefix(strings.ToLower(raw), "cve-") && flag.Type != "ucq" {
errs = append(errs, fmt.Errorf("CVE numbers are required to be UCQ with choice_cost")) errs = multierr.Append(errs, fmt.Errorf("CVE numbers are required to be UCQ with choice_cost"))
} }
if _, err := strconv.ParseInt(raw, 10, 64); flag.Type == "key" && err == nil && !exceptions.HasException(":not-number-flag") { if _, err := strconv.ParseInt(raw, 10, 64); flag.Type == "key" && err == nil && !exceptions.HasException(":not-number-flag") {
errs = append(errs, fmt.Errorf("shouldn't be this flag a number type? (:not-number-flag)")) errs = multierr.Append(errs, fmt.Errorf("shouldn't be this flag a number type? (:not-number-flag)"))
} }
if flag.Placeholder == "" && (strings.HasPrefix(flag.Type, "number") || flag.Type == "key" || flag.Type == "text" || (flag.Type == "ucq" && flag.ChoicesCost > 0)) { if flag.Placeholder == "" && (strings.HasPrefix(flag.Type, "number") || flag.Type == "key" || flag.Type == "text" || (flag.Type == "ucq" && flag.ChoicesCost > 0)) {
errs = append(errs, fmt.Errorf("no placeholder defined")) errs = multierr.Append(errs, fmt.Errorf("no placeholder defined"))
} }
if strings.HasPrefix(flag.Type, "number") { if strings.HasPrefix(flag.Type, "number") {
min, max, step, err := fic.AnalyzeNumberFlag(flag.Type) min, max, step, err := fic.AnalyzeNumberFlag(flag.Type)
if err != nil { if err != nil {
errs = append(errs, err) errs = multierr.Append(errs, err)
} else if min == nil || max == nil || step == nil { } else if min == nil || max == nil || step == nil {
errs = append(errs, fmt.Errorf("please define min and max for your number flag")) errs = multierr.Append(errs, fmt.Errorf("please define min and max for your number flag"))
} else if (*max-*min) / *step <= 10 { } else if (*max-*min) / *step <= 10 {
errs = append(errs, fmt.Errorf("to avoid bruteforce, define more than 10 possibilities")) errs = multierr.Append(errs, fmt.Errorf("to avoid bruteforce, define more than 10 possibilities"))
} }
} }
return return
} }
func EPITACheckKeyFlagWithChoices(flag *fic.FlagKey, raw string, choices []*fic.FlagChoice, _ *fic.Exercice, exceptions *sync.CheckExceptions) (errs []error) { func EPITACheckKeyFlagWithChoices(flag *fic.FlagKey, raw string, choices []*fic.FlagChoice, _ *fic.Exercice, exceptions *sync.CheckExceptions) (errs error) {
if !exceptions.HasException(":bruteforcable-choices") { if !exceptions.HasException(":bruteforcable-choices") {
if len(choices) < 10 && flag.ChoicesCost == 0 { if len(choices) < 10 && flag.ChoicesCost == 0 {
errs = append(errs, fmt.Errorf("requires at least 10 choices to avoid brute-force")) errs = multierr.Append(errs, fmt.Errorf("requires at least 10 choices to avoid brute-force"))
} else if len(choices) < 6 && flag.ChoicesCost > 0 { } else if len(choices) < 6 && flag.ChoicesCost > 0 {
errs = append(errs, fmt.Errorf("requires at least 10 choices to avoid brute-force")) errs = multierr.Append(errs, fmt.Errorf("requires at least 10 choices to avoid brute-force"))
} }
} }

View File

@ -4,17 +4,19 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
) )
func InspectFile(file *fic.EFile, _ *fic.Exercice, exceptions *sync.CheckExceptions) (errs []error) { func InspectFile(file *fic.EFile, _ *fic.Exercice, exceptions *sync.CheckExceptions) (errs error) {
if filepath.Ext(file.Name) == ".tar" || strings.HasSuffix(file.Name, ".tar.gz") || strings.HasSuffix(file.Name, ".tar.bz2") { if filepath.Ext(file.Name) == ".tar" || strings.HasSuffix(file.Name, ".tar.gz") || strings.HasSuffix(file.Name, ".tar.bz2") {
// Check there is more than 1 file in tarball // Check there is more than 1 file in tarball
errs = append(errs, checkTarball(file, exceptions)...) errs = multierr.Append(errs, checkTarball(file, exceptions))
} else if filepath.Ext(file.Name) == ".zip" { } else if filepath.Ext(file.Name) == ".zip" {
// Check there is more than 1 file in zip // Check there is more than 1 file in zip
errs = append(errs, checkZip(file, exceptions)...) errs = multierr.Append(errs, checkZip(file, exceptions))
} }
return return

View File

@ -9,11 +9,13 @@ import (
"log" "log"
"strings" "strings"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
) )
func checkTarball(file *fic.EFile, exceptions *sync.CheckExceptions) (errs []error) { func checkTarball(file *fic.EFile, exceptions *sync.CheckExceptions) (errs error) {
fd, closer, err := sync.GetFile(sync.GlobalImporter, file.GetOrigin()) fd, closer, err := sync.GetFile(sync.GlobalImporter, file.GetOrigin())
if err != nil { if err != nil {
log.Printf("Unable to open %q: %s", file.GetOrigin(), err.Error()) log.Printf("Unable to open %q: %s", file.GetOrigin(), err.Error())
@ -56,11 +58,11 @@ func checkTarball(file *fic.EFile, exceptions *sync.CheckExceptions) (errs []err
if nbFile < 2 { if nbFile < 2 {
if !exceptions.HasException(":one-file-tarball") { if !exceptions.HasException(":one-file-tarball") {
errs = append(errs, fmt.Errorf("don't make a tarball for one file")) errs = multierr.Append(errs, fmt.Errorf("don't make a tarball for one file"))
} }
} else if nbFile < 5 && false { } else if nbFile < 5 && false {
if !exceptions.HasException(":few-files-tarball") { if !exceptions.HasException(":few-files-tarball") {
errs = append(errs, fmt.Errorf("don't make a tarball for so little files (:few-files-tarball)")) errs = multierr.Append(errs, fmt.Errorf("don't make a tarball for so little files (:few-files-tarball)"))
} }
} else { } else {
log.Printf("%d files found in %q", nbFile, file.Name) log.Printf("%d files found in %q", nbFile, file.Name)

View File

@ -7,11 +7,13 @@ import (
"log" "log"
"strings" "strings"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
) )
func checkZip(file *fic.EFile, exceptions *sync.CheckExceptions) (errs []error) { func checkZip(file *fic.EFile, exceptions *sync.CheckExceptions) (errs error) {
fd, closer, err := sync.GetFile(sync.GlobalImporter, file.GetOrigin()) fd, closer, err := sync.GetFile(sync.GlobalImporter, file.GetOrigin())
if err != nil { if err != nil {
log.Printf("Unable to open %q: %s", file.GetOrigin(), err.Error()) log.Printf("Unable to open %q: %s", file.GetOrigin(), err.Error())
@ -39,11 +41,11 @@ func checkZip(file *fic.EFile, exceptions *sync.CheckExceptions) (errs []error)
if len(r.File) < 2 { if len(r.File) < 2 {
if !exceptions.HasException(":one-file-tarball") { if !exceptions.HasException(":one-file-tarball") {
errs = append(errs, fmt.Errorf("don't make a ZIP archive for one file, use gzip instead")) errs = multierr.Append(errs, fmt.Errorf("don't make a ZIP archive for one file, use gzip instead"))
} }
} else if len(r.File) < 5 && false { } else if len(r.File) < 5 && false {
if !exceptions.HasException(":few-files-tarball") { if !exceptions.HasException(":few-files-tarball") {
errs = append(errs, fmt.Errorf("don't make a ZIP archive for so little files (:few-files-tarball)")) errs = multierr.Append(errs, fmt.Errorf("don't make a ZIP archive for so little files (:few-files-tarball)"))
} }
} else { } else {
log.Printf("%d files found in %q", len(r.File), file.Name) log.Printf("%d files found in %q", len(r.File), file.Name)
@ -83,7 +85,7 @@ func checkZip(file *fic.EFile, exceptions *sync.CheckExceptions) (errs []error)
} }
if nbLinuxDirFound > 2 && !exceptions.HasException(":not-a-linux-rootfs") { if nbLinuxDirFound > 2 && !exceptions.HasException(":not-a-linux-rootfs") {
errs = append(errs, fmt.Errorf("don't use a ZIP archive to store an Unix file system, prefer a tarball (:not-a-linux-rootfs)")) errs = multierr.Append(errs, fmt.Errorf("don't use a ZIP archive to store an Unix file system, prefer a tarball (:not-a-linux-rootfs)"))
} }
return return

View File

@ -6,51 +6,53 @@ import (
"log" "log"
"unicode" "unicode"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
lib "srs.epita.fr/fic-server/repochecker/grammalecte/lib" lib "srs.epita.fr/fic-server/repochecker/grammalecte/lib"
) )
func GrammalecteCheckKeyFlag(flag *fic.FlagKey, raw string, exercice *fic.Exercice, exceptions *sync.CheckExceptions) (errs []error) { func GrammalecteCheckKeyFlag(flag *fic.FlagKey, raw string, exercice *fic.Exercice, exceptions *sync.CheckExceptions) (errs error) {
if !isRecognizedLanguage(exercice.Language) { if !isRecognizedLanguage(exercice.Language) {
return return
} }
label, _, _, _ := flag.AnalyzeFlagLabel() label, _, _, _ := flag.AnalyzeFlagLabel()
for _, err := range grammalecte("label ", label, -1, exceptions, &CommonOpts) { for _, err := range multierr.Errors(grammalecte("label ", label, -1, exceptions, &CommonOpts)) {
if e, ok := err.(lib.GrammarError); ok && e.RuleId == "poncfin_règle1" { if e, ok := err.(lib.GrammarError); ok && e.RuleId == "poncfin_règle1" {
continue continue
} }
errs = append(errs, err) errs = multierr.Append(errs, err)
} }
if len(flag.Help) > 0 { if len(flag.Help) > 0 {
errs = append(errs, grammalecte("help ", flag.Help, -1, exceptions, &CommonOpts)...) errs = multierr.Append(errs, grammalecte("help ", flag.Help, -1, exceptions, &CommonOpts))
} }
return return
} }
func GrammalecteCheckFlagChoice(choice *fic.FlagChoice, exercice *fic.Exercice, exceptions *sync.CheckExceptions) (errs []error) { func GrammalecteCheckFlagChoice(choice *fic.FlagChoice, exercice *fic.Exercice, exceptions *sync.CheckExceptions) (errs error) {
if isRecognizedLanguage(exercice.Language) { if isRecognizedLanguage(exercice.Language) {
errs = append(errs, grammalecte("label ", choice.Label, -1, exceptions, &CommonOpts)...) errs = multierr.Append(errs, grammalecte("label ", choice.Label, -1, exceptions, &CommonOpts))
} }
if len(errs) == 0 && !unicode.IsUpper(bytes.Runes([]byte(choice.Label))[0]) && !exceptions.HasException(":label_majuscule") { if len(multierr.Errors(errs)) == 0 && !unicode.IsUpper(bytes.Runes([]byte(choice.Label))[0]) && !exceptions.HasException(":label_majuscule") {
errs = append(errs, fmt.Errorf("%q nécessite une majuscule (:label_majuscule)", choice.Label)) errs = multierr.Append(errs, fmt.Errorf("%q nécessite une majuscule (:label_majuscule)", choice.Label))
} }
return return
} }
func GrammalecteCheckHint(hint *fic.EHint, exercice *fic.Exercice, exceptions *sync.CheckExceptions) (errs []error) { func GrammalecteCheckHint(hint *fic.EHint, exercice *fic.Exercice, exceptions *sync.CheckExceptions) (errs error) {
if len(hint.Title) > 0 { if len(hint.Title) > 0 {
if isRecognizedLanguage(exercice.Language) { if isRecognizedLanguage(exercice.Language) {
errs = append(errs, grammalecte("title ", hint.Title, -1, exceptions, &CommonOpts)...) errs = multierr.Append(errs, grammalecte("title ", hint.Title, -1, exceptions, &CommonOpts))
} }
if len(errs) == 0 && !unicode.IsUpper(bytes.Runes([]byte(hint.Title))[0]) && !exceptions.HasException(":title_majuscule") { if len(multierr.Errors(errs)) == 0 && !unicode.IsUpper(bytes.Runes([]byte(hint.Title))[0]) && !exceptions.HasException(":title_majuscule") {
errs = append(errs, fmt.Errorf("%q nécessite une majuscule (:title_majuscule)", hint.Title)) errs = multierr.Append(errs, fmt.Errorf("%q nécessite une majuscule (:title_majuscule)", hint.Title))
} }
} }
@ -59,7 +61,7 @@ func GrammalecteCheckHint(hint *fic.EHint, exercice *fic.Exercice, exceptions *s
return return
} }
func GrammalecteCheckGrammar(data interface{}, exceptions *sync.CheckExceptions) []error { func GrammalecteCheckGrammar(data interface{}, exceptions *sync.CheckExceptions) error {
if s, ok := data.(struct { if s, ok := data.(struct {
Str string Str string
Language string Language string

View File

@ -9,6 +9,8 @@ import (
"regexp" "regexp"
"strings" "strings"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
lib "srs.epita.fr/fic-server/repochecker/grammalecte/lib" lib "srs.epita.fr/fic-server/repochecker/grammalecte/lib"
) )
@ -119,7 +121,7 @@ var (
mdimg = regexp.MustCompile(`!\[([^\]]+)\]\([^)]+\)`) mdimg = regexp.MustCompile(`!\[([^\]]+)\]\([^)]+\)`)
) )
func grammalecte(name string, text string, paragraph int, exceptions *sync.CheckExceptions, options *GrammalecteOptions) (errs []error) { func grammalecte(name string, text string, paragraph int, exceptions *sync.CheckExceptions, options *GrammalecteOptions) (errs error) {
// Remove Markdown elements // Remove Markdown elements
text = mdimg.ReplaceAllString(text, "Image : ${1}") text = mdimg.ReplaceAllString(text, "Image : ${1}")
@ -173,7 +175,7 @@ func grammalecte(name string, text string, paragraph int, exceptions *sync.Check
} }
suggestions, _ := suggest(serror.Value) suggestions, _ := suggest(serror.Value)
errs = append(errs, lib.SpellingError{ errs = multierr.Append(errs, lib.SpellingError{
Prefix: name, Prefix: name,
Source: data.Text, Source: data.Text,
NSource: data.Paragraph, NSource: data.Paragraph,
@ -207,7 +209,7 @@ func grammalecte(name string, text string, paragraph int, exceptions *sync.Check
continue continue
} }
errs = append(errs, err) errs = multierr.Append(errs, err)
} }
} }

View File

@ -15,9 +15,10 @@ import (
"github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/text" "github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util" "github.com/yuin/goldmark/util"
"go.uber.org/multierr"
) )
func GrammalecteCheckMDText(str string, lang string, exceptions *sync.CheckExceptions, forbiddenStrings ...string) (errs []error) { func GrammalecteCheckMDText(str string, lang string, exceptions *sync.CheckExceptions, forbiddenStrings ...string) (errs error) {
if !isRecognizedLanguage(lang) { if !isRecognizedLanguage(lang) {
return return
} }
@ -34,7 +35,7 @@ func GrammalecteCheckMDText(str string, lang string, exceptions *sync.CheckExcep
for _, s := range forbiddenStrings { for _, s := range forbiddenStrings {
if strings.Contains(str, s) { if strings.Contains(str, s) {
if !exceptions.HasException(":not-forbidden-string:" + s) { if !exceptions.HasException(":not-forbidden-string:" + s) {
errs = append(errs, fmt.Errorf("Forbidden raw string %q included in file content, don't write it (:not-forbidden-string:%s)", s, s)) errs = multierr.Append(errs, fmt.Errorf("Forbidden raw string %q included in file content, don't write it (:not-forbidden-string:%s)", s, s))
} }
} }
} }
@ -62,13 +63,13 @@ func GrammalecteCheckMDText(str string, lang string, exceptions *sync.CheckExcep
var buf bytes.Buffer var buf bytes.Buffer
if err := markdown.Convert([]byte(str), &buf); err != nil { if err := markdown.Convert([]byte(str), &buf); err != nil {
errs = append(errs, err) errs = multierr.Append(errs, err)
} }
errs = append(errs, checker.errs...) errs = multierr.Append(errs, checker.errs)
errs = append(errs, voidRenderer.Errors()...) errs = multierr.Append(errs, voidRenderer.Errors())
for _, err := range grammalecte("", buf.String(), 0, exceptions, &CommonOpts) { for _, err := range multierr.Errors(grammalecte("", buf.String(), 0, exceptions, &CommonOpts)) {
if gerror, ok := err.(lib.GrammarError); ok { if gerror, ok := err.(lib.GrammarError); ok {
if (gerror.RuleId == "redondances_paragraphe" || gerror.RuleId == "redondances_phrase") && gerror.GetPassage() == "SubstitutDeCode" { if (gerror.RuleId == "redondances_paragraphe" || gerror.RuleId == "redondances_phrase") && gerror.GetPassage() == "SubstitutDeCode" {
continue continue
@ -79,7 +80,7 @@ func GrammalecteCheckMDText(str string, lang string, exceptions *sync.CheckExcep
} }
} }
errs = append(errs, err) errs = multierr.Append(errs, err)
} }
return return
@ -87,7 +88,7 @@ func GrammalecteCheckMDText(str string, lang string, exceptions *sync.CheckExcep
type grammarChecker struct { type grammarChecker struct {
exceptions *sync.CheckExceptions exceptions *sync.CheckExceptions
errs []error errs error
} }
func (t *grammarChecker) Transform(doc *ast.Document, reader text.Reader, pc parser.Context) { func (t *grammarChecker) Transform(doc *ast.Document, reader text.Reader, pc parser.Context) {
@ -99,11 +100,11 @@ func (t *grammarChecker) Transform(doc *ast.Document, reader text.Reader, pc par
switch child := node.(type) { switch child := node.(type) {
case *ast.Image: case *ast.Image:
if len(child.Title) > 0 { if len(child.Title) > 0 {
t.errs = append(t.errs, grammalecte("", string(child.Title), 0, t.exceptions, &CommonOpts)...) t.errs = multierr.Append(t.errs, grammalecte("", string(child.Title), 0, t.exceptions, &CommonOpts))
} }
case *ast.Link: case *ast.Link:
if len(child.Title) > 0 { if len(child.Title) > 0 {
t.errs = append(t.errs, grammalecte("", string(child.Title), 0, t.exceptions, &CommonOpts)...) t.errs = multierr.Append(t.errs, grammalecte("", string(child.Title), 0, t.exceptions, &CommonOpts))
} }
} }

View File

@ -7,10 +7,11 @@ import (
"github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/ast"
goldrender "github.com/yuin/goldmark/renderer" goldrender "github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/util" "github.com/yuin/goldmark/util"
"go.uber.org/multierr"
) )
type VoidRenderer struct { type VoidRenderer struct {
errs []error errs error
} }
func NewVoidRenderer() *VoidRenderer { func NewVoidRenderer() *VoidRenderer {
@ -28,7 +29,7 @@ func (r *VoidRenderer) RegisterFuncs(reg goldrender.NodeRendererFuncRegisterer)
reg.Register(ast.KindString, r.renderString) reg.Register(ast.KindString, r.renderString)
} }
func (r *VoidRenderer) Errors() []error { func (r *VoidRenderer) Errors() error {
return r.errs return r.errs
} }
@ -77,7 +78,7 @@ func (r *VoidRenderer) renderImage(w util.BufWriter, source []byte, node ast.Nod
// Check there is a correct image alt // Check there is a correct image alt
alt := nodeToText(n, source) alt := nodeToText(n, source)
if len(bytes.Fields(alt)) <= 1 { if len(bytes.Fields(alt)) <= 1 {
r.errs = append(r.errs, fmt.Errorf("No valid image alternative defined for %q", n.Destination)) r.errs = multierr.Append(r.errs, fmt.Errorf("No valid image alternative defined for %q", n.Destination))
return ast.WalkContinue, nil return ast.WalkContinue, nil
} }

View File

@ -15,6 +15,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
) )
@ -108,14 +110,14 @@ func searchBinaryInGit(edir string) (ret []string) {
return return
} }
func checkExercice(theme *fic.Theme, edir string, dmap *map[int64]*fic.Exercice, exceptions *sync.CheckExceptions) (errs []error) { func checkExercice(theme *fic.Theme, edir string, dmap *map[int64]*fic.Exercice, exceptions *sync.CheckExceptions) (errs error) {
e, _, eid, exceptions, _, berrs := sync.BuildExercice(sync.GlobalImporter, theme, path.Join(theme.Path, edir), dmap, nil) e, _, eid, exceptions, _, berrs := sync.BuildExercice(sync.GlobalImporter, theme, path.Join(theme.Path, edir), dmap, nil)
errs = append(errs, berrs...) errs = multierr.Append(errs, berrs)
if e != nil { if e != nil {
// Files // Files
var files []string var files []string
var cerrs []error var cerrs error
if !skipFileChecks { if !skipFileChecks {
files, cerrs = sync.CheckExerciceFiles(sync.GlobalImporter, e, exceptions) files, cerrs = sync.CheckExerciceFiles(sync.GlobalImporter, e, exceptions)
log.Printf("%d files checked.\n", len(files)) log.Printf("%d files checked.\n", len(files))
@ -123,16 +125,16 @@ func checkExercice(theme *fic.Theme, edir string, dmap *map[int64]*fic.Exercice,
files, cerrs = sync.CheckExerciceFilesPresence(sync.GlobalImporter, e) files, cerrs = sync.CheckExerciceFilesPresence(sync.GlobalImporter, e)
log.Printf("%d files presents but not checked (please check digest yourself).\n", len(files)) log.Printf("%d files presents but not checked (please check digest yourself).\n", len(files))
} }
errs = append(errs, cerrs...) errs = multierr.Append(errs, cerrs)
// Flags // Flags
flags, cerrs := sync.CheckExerciceFlags(sync.GlobalImporter, e, files, exceptions) flags, cerrs := sync.CheckExerciceFlags(sync.GlobalImporter, e, files, exceptions)
errs = append(errs, cerrs...) errs = multierr.Append(errs, cerrs)
log.Printf("%d flags checked.\n", len(flags)) log.Printf("%d flags checked.\n", len(flags))
// Hints // Hints
hints, cerrs := sync.CheckExerciceHints(sync.GlobalImporter, e, exceptions) hints, cerrs := sync.CheckExerciceHints(sync.GlobalImporter, e, exceptions)
errs = append(errs, cerrs...) errs = multierr.Append(errs, cerrs)
log.Printf("%d hints checked.\n", len(hints)) log.Printf("%d hints checked.\n", len(hints))
if dmap != nil { if dmap != nil {
@ -247,8 +249,9 @@ func main() {
theme, exceptions, errs := sync.BuildTheme(sync.GlobalImporter, p) theme, exceptions, errs := sync.BuildTheme(sync.GlobalImporter, p)
if theme != nil && !sync.GlobalImporter.Exists(path.Join(p, "challenge.txt")) && !sync.GlobalImporter.Exists(path.Join(p, "challenge.toml")) { if theme != nil && !sync.GlobalImporter.Exists(path.Join(p, "challenge.txt")) && !sync.GlobalImporter.Exists(path.Join(p, "challenge.toml")) {
nberr += len(errs) thiserrors := multierr.Errors(errs)
for _, err := range errs { nberr += len(thiserrors)
for _, err := range thiserrors {
log.Println(err) log.Println(err)
} }
@ -264,7 +267,7 @@ func main() {
for _, edir := range exercices { for _, edir := range exercices {
ex_exceptions := exceptions.GetFileExceptions(edir) ex_exceptions := exceptions.GetFileExceptions(edir)
for _, err := range checkExercice(theme, edir, &dmap, ex_exceptions) { for _, err := range multierr.Errors(checkExercice(theme, edir, &dmap, ex_exceptions)) {
log.Println(err.Error()) log.Println(err.Error())
if logMissingResolution { if logMissingResolution {
@ -294,7 +297,7 @@ func main() {
} else { } else {
log.Printf("This is not a theme directory, run checks for exercice.\n\n") log.Printf("This is not a theme directory, run checks for exercice.\n\n")
for _, err := range checkExercice(&fic.Theme{}, p, &map[int64]*fic.Exercice{}, nil) { for _, err := range multierr.Errors(checkExercice(&fic.Theme{}, p, &map[int64]*fic.Exercice{}, nil)) {
nberr += 1 nberr += 1
log.Println(err) log.Println(err)
} }

View File

@ -42,7 +42,7 @@ func (pcapNGReader *PcapNgReader) ReadPacketData() (data []byte, ci gopacket.Cap
// Iterate thought each packet to find potentialy unwanted packets // Iterate thought each packet to find potentialy unwanted packets
// TODO: Allow custom rules to specify what is a unwanted packet // TODO: Allow custom rules to specify what is a unwanted packet
func CheckPcap(pcapReader PcapPacketDataReader, pcapName string) (errs []error) { func CheckPcap(pcapReader PcapPacketDataReader, pcapName string) (errs error) {
warningFlows := make(map[gopacket.Flow]([]time.Time)) warningFlows := make(map[gopacket.Flow]([]time.Time))
// //
@ -105,11 +105,11 @@ func CheckPcap(pcapReader PcapPacketDataReader, pcapName string) (errs []error)
return return
} }
func CheckTextFile(fd *os.File) (errs []error) { func CheckTextFile(fd *os.File) (errs error) {
return return
} }
func InspectFileForIPAddr(file *fic.EFile, _ *fic.Exercice, exceptions *sync.CheckExceptions) (errs []error) { func InspectFileForIPAddr(file *fic.EFile, _ *fic.Exercice, exceptions *sync.CheckExceptions) (errs error) {
fd, closer, err := sync.GetFile(sync.GlobalImporter, file.GetOrigin()) fd, closer, err := sync.GetFile(sync.GlobalImporter, file.GetOrigin())
if err != nil { if err != nil {
log.Printf("Unable to open %q: %s", file.GetOrigin(), err.Error()) log.Printf("Unable to open %q: %s", file.GetOrigin(), err.Error())

View File

@ -3,6 +3,8 @@ package main
import ( import (
"fmt" "fmt"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
) )
@ -15,9 +17,9 @@ func RegisterChecksHooks(h *sync.CheckHooks) {
h.RegisterExerciceHook(CheckResolutionVideo) h.RegisterExerciceHook(CheckResolutionVideo)
} }
func CheckResolutionVideo(e *fic.Exercice, exceptions *sync.CheckExceptions) (errs []error) { func CheckResolutionVideo(e *fic.Exercice, exceptions *sync.CheckExceptions) (errs error) {
for _, err := range checkResolutionVideo(e, exceptions) { for _, err := range multierr.Errors(checkResolutionVideo(e, exceptions)) {
errs = append(errs, fmt.Errorf("resolution.mp4: %w", err)) errs = multierr.Append(errs, fmt.Errorf("resolution.mp4: %w", err))
} }
return return

View File

@ -9,14 +9,15 @@ import (
"github.com/asticode/go-astisub" "github.com/asticode/go-astisub"
ffmpeg "github.com/u2takey/ffmpeg-go" ffmpeg "github.com/u2takey/ffmpeg-go"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
) )
func CheckGrammarSubtitleTrack(path string, index uint, lang string, exceptions *sync.CheckExceptions) (errs []error) { func CheckGrammarSubtitleTrack(path string, index uint, lang string, exceptions *sync.CheckExceptions) (errs error) {
tmpfile, err := ioutil.TempFile("", "resolution-*.srt") tmpfile, err := ioutil.TempFile("", "resolution-*.srt")
if err != nil { if err != nil {
errs = append(errs, fmt.Errorf("unable to create a temporary file: %w", err)) errs = multierr.Append(errs, fmt.Errorf("unable to create a temporary file: %w", err))
return return
} }
defer os.Remove(tmpfile.Name()) defer os.Remove(tmpfile.Name())
@ -26,7 +27,7 @@ func CheckGrammarSubtitleTrack(path string, index uint, lang string, exceptions
Output(tmpfile.Name(), ffmpeg.KwArgs{"map": fmt.Sprintf("0:%d", index)}). Output(tmpfile.Name(), ffmpeg.KwArgs{"map": fmt.Sprintf("0:%d", index)}).
OverWriteOutput().Run() OverWriteOutput().Run()
if err != nil { if err != nil {
errs = append(errs, fmt.Errorf("ffmpeg returns an error when extracting subtitles track: %w", err)) errs = multierr.Append(errs, fmt.Errorf("ffmpeg returns an error when extracting subtitles track: %w", err))
} }
subtitles, err := astisub.OpenFile(tmpfile.Name()) subtitles, err := astisub.OpenFile(tmpfile.Name())
@ -39,11 +40,11 @@ func CheckGrammarSubtitleTrack(path string, index uint, lang string, exceptions
for _, item := range subtitles.Items { for _, item := range subtitles.Items {
lines = append(lines, item.String()) lines = append(lines, item.String())
} }
for _, e := range hooks.CallCustomHook("CheckGrammar", struct { for _, e := range multierr.Errors(hooks.CallCustomHook("CheckGrammar", struct {
Str string Str string
Language string Language string
}{Str: strings.Join(lines, "\n"), Language: lang[:2]}, exceptions) { }{Str: strings.Join(lines, "\n"), Language: lang[:2]}, exceptions)) {
errs = append(errs, fmt.Errorf("subtitle-track: %w", e)) errs = multierr.Append(errs, fmt.Errorf("subtitle-track: %w", e))
} }
return return

View File

@ -8,6 +8,7 @@ import (
"strconv" "strconv"
ffmpeg "github.com/u2takey/ffmpeg-go" ffmpeg "github.com/u2takey/ffmpeg-go"
"go.uber.org/multierr"
"srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic" "srs.epita.fr/fic-server/libfic"
@ -49,7 +50,7 @@ func gcd(a, b int) int {
return bgcd(a, b, 1) return bgcd(a, b, 1)
} }
func checkResolutionVideo(e *fic.Exercice, exceptions *sync.CheckExceptions) (errs []error) { func checkResolutionVideo(e *fic.Exercice, exceptions *sync.CheckExceptions) (errs error) {
i, ok := sync.GlobalImporter.(sync.LocalImporter) i, ok := sync.GlobalImporter.(sync.LocalImporter)
if !ok { if !ok {
log.Printf("Unable to load `videos-rules.so` as the current Importer is not a LocalImporter (%T).", sync.GlobalImporter) log.Printf("Unable to load `videos-rules.so` as the current Importer is not a LocalImporter (%T).", sync.GlobalImporter)
@ -71,7 +72,7 @@ func checkResolutionVideo(e *fic.Exercice, exceptions *sync.CheckExceptions) (er
data, err := ffmpeg.Probe(path) data, err := ffmpeg.Probe(path)
if err != nil { if err != nil {
errs = append(errs, fmt.Errorf("unable to open %q: %w", path, err)) errs = multierr.Append(errs, fmt.Errorf("unable to open %q: %w", path, err))
return return
} }
@ -87,79 +88,79 @@ func checkResolutionVideo(e *fic.Exercice, exceptions *sync.CheckExceptions) (er
if s.CodecType == "video" { if s.CodecType == "video" {
video_seen = append(video_seen, idx) video_seen = append(video_seen, idx)
if (s.Width > 1920 || s.Height > 1080) && !exceptions.HasException(":size:above_maximum") { if (s.Width > 1920 || s.Height > 1080) && !exceptions.HasException(":size:above_maximum") {
errs = append(errs, fmt.Errorf("video track is too wide: %dx%d (maximum allowed: 1920x1080)", s.Width, s.Height)) errs = multierr.Append(errs, fmt.Errorf("video track is too wide: %dx%d (maximum allowed: 1920x1080)", s.Width, s.Height))
} }
ratio := s.Width * 10 / s.Height ratio := s.Width * 10 / s.Height
if ratio < 13 || ratio > 19 && !exceptions.HasException(":size:strange_ratio") { if ratio < 13 || ratio > 19 && !exceptions.HasException(":size:strange_ratio") {
m := gcd(s.Width, s.Height) m := gcd(s.Width, s.Height)
errs = append(errs, fmt.Errorf("video track has a strange ratio: %d:%d. Is this really expected?", s.Width/m, s.Height/m)) errs = multierr.Append(errs, fmt.Errorf("video track has a strange ratio: %d:%d. Is this really expected?", s.Width/m, s.Height/m))
} }
if s.CodecName != "h264" { if s.CodecName != "h264" {
errs = append(errs, fmt.Errorf("video codec has to be H264 (currently: %s)", s.CodecLongName)) errs = multierr.Append(errs, fmt.Errorf("video codec has to be H264 (currently: %s)", s.CodecLongName))
} }
duration, err := strconv.ParseFloat(s.Duration, 64) duration, err := strconv.ParseFloat(s.Duration, 64)
if err == nil { if err == nil {
if duration < 45 && !exceptions.HasException(":duration:too_short") { if duration < 45 && !exceptions.HasException(":duration:too_short") {
errs = append(errs, fmt.Errorf("video is too short")) errs = multierr.Append(errs, fmt.Errorf("video is too short"))
} }
if duration > 450 && !exceptions.HasException(":duration:too_long") { if duration > 450 && !exceptions.HasException(":duration:too_long") {
errs = append(errs, fmt.Errorf("video is too long")) errs = multierr.Append(errs, fmt.Errorf("video is too long"))
} }
} else { } else {
errs = append(errs, fmt.Errorf("invalid track duration: %q", s.Duration)) errs = multierr.Append(errs, fmt.Errorf("invalid track duration: %q", s.Duration))
} }
} else if s.CodecType == "subtitle" { } else if s.CodecType == "subtitle" {
subtitles_seen = append(subtitles_seen, idx) subtitles_seen = append(subtitles_seen, idx)
if s.CodecName != "mov_text" { if s.CodecName != "mov_text" {
errs = append(errs, fmt.Errorf("subtitle format has to be MOV text/3GPP Timed Text (currently: %s)", s.CodecLongName)) errs = multierr.Append(errs, fmt.Errorf("subtitle format has to be MOV text/3GPP Timed Text (currently: %s)", s.CodecLongName))
} }
nbframes, err := strconv.ParseInt(s.NbFrames, 10, 64) nbframes, err := strconv.ParseInt(s.NbFrames, 10, 64)
if err == nil { if err == nil {
if nbframes < 5 && !exceptions.HasException(":subtitle:tiny") { if nbframes < 5 && !exceptions.HasException(":subtitle:tiny") {
errs = append(errs, fmt.Errorf("too few subtitles")) errs = multierr.Append(errs, fmt.Errorf("too few subtitles"))
} }
} else { } else {
errs = append(errs, fmt.Errorf("invalid number of frame: %q", s.NbFrames)) errs = multierr.Append(errs, fmt.Errorf("invalid number of frame: %q", s.NbFrames))
} }
} else if s.CodecType == "audio" { } else if s.CodecType == "audio" {
if !exceptions.HasException(":audio:allowed") { if !exceptions.HasException(":audio:allowed") {
errs = append(errs, fmt.Errorf("an audio track is present, use subtitle for explainations")) errs = multierr.Append(errs, fmt.Errorf("an audio track is present, use subtitle for explainations"))
} }
if s.CodecName != "aac" { if s.CodecName != "aac" {
errs = append(errs, fmt.Errorf("audio codec has to be AAC (Advanced Audio Coding) (currently: %s)", s.CodecLongName)) errs = multierr.Append(errs, fmt.Errorf("audio codec has to be AAC (Advanced Audio Coding) (currently: %s)", s.CodecLongName))
} }
} else { } else {
errs = append(errs, fmt.Errorf("unknown track found of type %q", s.CodecType)) errs = multierr.Append(errs, fmt.Errorf("unknown track found of type %q", s.CodecType))
} }
} }
if len(video_seen) == 0 { if len(video_seen) == 0 {
errs = append(errs, fmt.Errorf("no video track found")) errs = multierr.Append(errs, fmt.Errorf("no video track found"))
} else if len(video_seen) > 1 { } else if len(video_seen) > 1 {
errs = append(errs, fmt.Errorf("%d video tracks found, is it expected?", len(video_seen))) errs = multierr.Append(errs, fmt.Errorf("%d video tracks found, is it expected?", len(video_seen)))
} }
if len(subtitles_seen) == 0 && !exceptions.HasException(":subtitle:no_track") { if len(subtitles_seen) == 0 && !exceptions.HasException(":subtitle:no_track") {
errs = append(errs, fmt.Errorf("no subtitles track found")) errs = multierr.Append(errs, fmt.Errorf("no subtitles track found"))
} else if len(subtitles_seen) > 0 { } else if len(subtitles_seen) > 0 {
for _, idx := range subtitles_seen { for _, idx := range subtitles_seen {
language := e.Language language := e.Language
if lang, ok := vInfo.Streams[idx].Tags["language"]; e.Language != "" && (!ok || lang == "" || lang == "und") { if lang, ok := vInfo.Streams[idx].Tags["language"]; e.Language != "" && (!ok || lang == "" || lang == "und") {
errs = append(errs, fmt.Errorf("subtitles track %d with no language defined", vInfo.Streams[idx].Index)) errs = multierr.Append(errs, fmt.Errorf("subtitles track %d with no language defined", vInfo.Streams[idx].Index))
} else { } else {
language = lang language = lang
} }
errs = append(errs, CheckGrammarSubtitleTrack(path, vInfo.Streams[idx].Index, language, exceptions)...) errs = multierr.Append(errs, CheckGrammarSubtitleTrack(path, vInfo.Streams[idx].Index, language, exceptions))
} }
if e.Language != "" && len(subtitles_seen) < 2 { if e.Language != "" && len(subtitles_seen) < 2 {
errs = append(errs, fmt.Errorf("subtitle tracks must exist in original language and translated, only one subtitle track found")) errs = multierr.Append(errs, fmt.Errorf("subtitle tracks must exist in original language and translated, only one subtitle track found"))
} }
} }