package sync import ( "bufio" "crypto" "encoding/hex" "fmt" "io" "net/http" "os" "path" "strings" "github.com/gin-gonic/gin" "go.uber.org/multierr" _ "golang.org/x/crypto/blake2b" "srs.epita.fr/fic-server/libfic" ) type importHint struct { Line int Hint *fic.EHint FlagsDeps []int64 } func buildExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) (hints []importHint, errs error) { params, _, err := parseExerciceParams(i, exercice.Path) if err != nil { errs = multierr.Append(errs, NewChallengeTxtError(exercice, 0, err)) return } for n, hint := range params.Hints { h := &fic.EHint{} if hint.Title == "" { h.Title = fmt.Sprintf("Astuce #%d", n+1) } else { h.Title = fixnbsp(hint.Title) } if hint.Cost == nil { h.Cost = exercice.Gain / 4 } else { h.Cost = *hint.Cost } if hint.Filename != "" { if hint.Content != "" { errs = multierr.Append(errs, NewHintError(exercice, h, n, fmt.Errorf("content and filename can't be filled at the same time"))) continue } else if !i.Exists(path.Join(exercice.Path, "files", hint.Filename)) { errs = multierr.Append(errs, NewHintError(exercice, h, n, fmt.Errorf("%q: File not found", hint.Filename))) continue } else { // Handle files as downloadable content if res, err := i.importFile(path.Join(exercice.Path, "files", hint.Filename), func(filePath string, origin string) (interface{}, error) { // Calculate hash hash512 := crypto.BLAKE2b_512.New() if fd, err := os.Open(filePath); err != nil { return nil, err } else { defer fd.Close() reader := bufio.NewReader(fd) if _, err := io.Copy(hash512, reader); err != nil { return nil, err } } result512 := hash512.Sum(nil) // Special format for downloadable hints: $FILES + hexhash + path from FILES/ return "$FILES" + hex.EncodeToString(result512) + strings.TrimPrefix(filePath, fic.FilesDir), nil }); err != nil { errs = multierr.Append(errs, NewHintError(exercice, h, n, fmt.Errorf("%q: unable to import hint file: %w", hint.Filename, err))) continue } else if s, ok := res.(string); !ok { errs = multierr.Append(errs, NewHintError(exercice, h, n, fmt.Errorf("%q: unable to import hint file: invalid string returned as filename", hint.Filename))) continue } else { h.Content = s } } } else if hint.Content == "" { errs = multierr.Append(errs, NewHintError(exercice, h, n, fmt.Errorf("content and filename can't be empty at the same time"))) continue } else { // Call checks hooks for _, hk := range hooks.mdTextHooks { for _, err := range multierr.Errors(hk(h.Content, exercice.Language, exceptions)) { errs = multierr.Append(errs, NewHintError(exercice, h, n, err)) } } if h.Content, err = ProcessMarkdown(i, fixnbsp(hint.Content), exercice.Path); err != nil { errs = multierr.Append(errs, NewHintError(exercice, h, n, fmt.Errorf("error during markdown formating: %w", err))) } } // Call checks hooks for _, hook := range hooks.hintHooks { for _, e := range multierr.Errors(hook(h, exercice, exceptions)) { errs = multierr.Append(errs, NewHintError(exercice, h, n, e)) } } newHint := importHint{ Line: n + 1, Hint: h, } // Read dependency to flag for _, nf := range hint.NeedFlag { newHint.FlagsDeps = append(newHint.FlagsDeps, nf.Id) } hints = append(hints, newHint) } return } // CheckExerciceHints checks if all hints are corrects.. func CheckExerciceHints(i Importer, exercice *fic.Exercice, exceptions *CheckExceptions) ([]importHint, error) { exceptions = exceptions.GetFileExceptions("challenge.toml", "challenge.txt") hints, errs := buildExerciceHints(i, exercice, exceptions) for _, hint := range hints { if hint.Hint.Cost >= exercice.Gain { errs = multierr.Append(errs, NewHintError(exercice, hint.Hint, hint.Line, fmt.Errorf("hint's cost is higher than exercice gain"))) } } return hints, errs } // SyncExerciceHints reads the content of files/ 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) { if _, err := exercice.WipeHints(); err != nil { errs = multierr.Append(errs, err) } else { exceptions = exceptions.GetFileExceptions("challenge.toml", "challenge.txt") hints, berrs := buildExerciceHints(i, exercice, exceptions) errs = multierr.Append(errs, berrs) hintsBindings = map[int]*fic.EHint{} for _, hint := range hints { // Import hint if h, err := exercice.AddHint(hint.Hint.Title, hint.Hint.Content, hint.Hint.Cost); err != nil { errs = multierr.Append(errs, NewHintError(exercice, hint.Hint, hint.Line, err)) } else { hintsBindings[hint.Line] = h // Handle hints dependencies on flags for _, nf := range hint.FlagsDeps { if f, ok := flagsBindings[nf]; ok { if herr := h.AddDepend(f); herr != nil { errs = multierr.Append(errs, NewHintError(exercice, hint.Hint, hint.Line, fmt.Errorf("error hint dependency to flag #%d: %w", nf, herr))) } } else { errs = multierr.Append(errs, NewHintError(exercice, hint.Hint, hint.Line, fmt.Errorf("error hint dependency to flag #%d: Unexistant flag", nf))) } } } } } return } // ApiListRemoteExerciceHints is an accessor letting foreign packages to access remote exercice hints. func ApiGetRemoteExerciceHints(c *gin.Context) { theme, exceptions, errs := BuildTheme(GlobalImporter, c.Params.ByName("thid")) if theme != nil { exercice, _, _, eexceptions, _, errs := BuildExercice(GlobalImporter, theme, path.Join(theme.Path, c.Params.ByName("exid")), nil, exceptions) if exercice != nil { hints, errs := CheckExerciceHints(GlobalImporter, exercice, eexceptions) if hints != nil { c.JSON(http.StatusOK, hints) return } c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs)) return } c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs)) return } c.AbortWithStatusJSON(http.StatusInternalServerError, fmt.Errorf("%q", errs)) }