server/admin/sync/exercice_keys.go

319 lines
9.6 KiB
Go

package sync
import (
"fmt"
"math/rand"
"path"
"sort"
"strings"
"unicode"
"srs.epita.fr/fic-server/libfic"
)
// isFullGraphic detects if some rune are not graphic one.
// This function is usefull to display warning when importing key ending with \r.
func isFullGraphic(s string) bool {
for _, c := range s {
if !unicode.IsGraphic(c) {
return false
}
}
return true
}
func validatorRegexp(vre string) (validator_regexp *string) {
if len(vre) > 0 {
validator_regexp = &vre
} else {
validator_regexp = nil
}
return
}
func getRawKey(flag ExerciceFlag) (raw string, prep string, errs []string) {
// Concatenate array
if f, ok := flag.Raw.([]interface{}); ok {
if len(flag.ValidatorRe) > 0 {
errs = append(errs, "ValidatorRe cannot be defined for this kind of flag.")
flag.ValidatorRe = ""
}
if len(flag.Separator) == 0 {
flag.Separator = ","
} else if len(flag.Separator) > 1 {
flag.Separator = string(flag.Separator[0])
errs = append(errs, "separator truncated to %q")
}
var fitems []string
for _, v := range f {
if g, ok := v.(string); ok {
if strings.Index(g, flag.Separator) != -1 {
errs = append(errs, "flag items cannot contain %q character as it is used as separator. Change the separator attribute for this flag.")
return
} else {
fitems = append(fitems, g)
}
} else {
errs = append(errs, "item %d has an invalid type: can only be string, is %T.")
return
}
}
ignord := "t"
if flag.Ordered {
sort.Strings(fitems)
ignord = "f"
}
raw = strings.Join(fitems, flag.Separator) + flag.Separator
prep = "`" + flag.Separator + ignord
} else if f, ok := flag.Raw.(int64); ok {
raw = fmt.Sprintf("%d", f)
} else if f, ok := flag.Value.(string); ok && len(f) > 0 {
raw = f
} else if f, ok := flag.Raw.(string); !ok {
errs = append(errs, fmt.Sprintf("has an invalid type: can only be []string or string, not %T", flag.Raw))
return
} else {
raw = f
}
return
}
// SyncExerciceFlags reads the content of challenge.txt and import "classic" flags as Key for the given challenge.
func SyncExerciceFlags(i Importer, exercice fic.Exercice) (errs []string) {
if _, err := exercice.WipeFlags(); err != nil {
errs = append(errs, err.Error())
} else if _, err := exercice.WipeMCQs(); err != nil {
errs = append(errs, err.Error())
} else if params, err := parseExerciceParams(i, exercice.Path); err != nil {
errs = append(errs, fmt.Sprintf("%q: challenge.txt: %s", path.Base(exercice.Path), err))
} else if len(params.Flags) == 0 && len(params.FlagsUCQ) == 0 && len(params.FlagsMCQ) == 0 {
errs = append(errs, fmt.Sprintf("%q: has no flag", path.Base(exercice.Path)))
} else {
kmap := map[int64]fic.Flag{}
// Import UCQ flags
for _, flag := range params.FlagsUCQ {
params.Flags = append(params.Flags, ExerciceFlag{
Id: flag.Id,
Label: flag.Label,
Type: "ucq",
Raw: flag.Raw,
ValidatorRe: flag.ValidatorRe,
Help: flag.Help,
ChoicesCost: flag.ChoicesCost,
Choice: flag.Choice,
LockedFile: flag.LockedFile,
NeedFlag: flag.NeedFlag,
})
}
// Import MCQ flags
for _, flag := range params.FlagsMCQ {
params.Flags = append(params.Flags, ExerciceFlag{
Id: flag.Id,
Label: flag.Label,
Type: "mcq",
ChoicesCost: flag.ChoicesCost,
Choice: flag.Choice,
LockedFile: flag.LockedFile,
NeedFlag: flag.NeedFlag,
})
}
// Import normal flags
for nline, flag := range params.Flags {
if len(flag.Label) == 0 {
flag.Label = "Flag"
}
if flag.Label[0] == '`' {
errs = append(errs, fmt.Sprintf("%q: flag #%d: Label should not begin with `.", path.Base(exercice.Path), nline + 1))
flag.Label = flag.Label[1:]
}
switch strings.ToLower(flag.Type) {
case "":
flag.Type = "key"
case "key":
flag.Type = "key"
case "ucq":
flag.Type = "ucq"
case "mcq":
flag.Type = "mcq"
default:
errs = append(errs, fmt.Sprintf("%q: flag #%d: invalid type of flag: should be 'key', 'mcq' or 'ucq'.", path.Base(exercice.Path), nline + 1))
continue
}
var addedFlag fic.Flag
if flag.Type == "key" || flag.Type == "ucq" {
raw, prep, terrs := getRawKey(flag)
if len(terrs) > 0 {
for _, err := range terrs {
errs = append(errs, fmt.Sprintf("%q: flag #%d: %s", path.Base(exercice.Path), nline + 1, err))
}
continue
}
flag.Label = prep + flag.Label
if !isFullGraphic(raw) {
errs = append(errs, fmt.Sprintf("%q: WARNING flag #%d: non-printable characters in flag, is this really expected?", path.Base(exercice.Path), nline + 1))
}
if k, err := exercice.AddRawFlagKey(flag.Label, flag.Help, flag.IgnoreCase, validatorRegexp(flag.ValidatorRe), []byte(raw), flag.ChoicesCost); err != nil {
errs = append(errs, fmt.Sprintf("%q: error flag #%d: %s", path.Base(exercice.Path), nline + 1, err))
continue
} else {
addedFlag = k
if len(flag.Choice) > 0 || flag.Type == "ucq" {
// Import choices
hasOne := false
if !flag.NoShuffle {
rand.Shuffle(len(flag.Choice), func(i, j int) {
flag.Choice[i], flag.Choice[j] = flag.Choice[j], flag.Choice[i]
})
}
for cid, choice := range flag.Choice {
val, prep, terrs := getRawKey(choice)
if len(terrs) > 0 {
for _, err := range terrs {
errs = append(errs, fmt.Sprintf("%q: flag UCQ #%d: %s", path.Base(exercice.Path), nline + 1, err))
}
continue
}
choice.Label = prep + choice.Label
if len(choice.Label) == 0 {
choice.Label = val
}
if _, err := k.AddChoice(choice.Label, val); err != nil {
errs = append(errs, fmt.Sprintf("%q: error in UCQ %d choice %d: %s", path.Base(exercice.Path), nline + 1, cid, err))
continue
}
if val == raw {
hasOne = true
}
}
if !hasOne {
errs = append(errs, fmt.Sprintf("%q: error in UCQ %d: no valid answer defined.", path.Base(exercice.Path), nline + 1))
}
}
}
} else if flag.Type == "mcq" {
if f, err := exercice.AddMCQ(flag.Label); err != nil {
errs = append(errs, fmt.Sprintf("%q: error flag MCQ #%d: %s", path.Base(exercice.Path), nline + 1, err))
continue
} else {
addedFlag = f
hasOne := false
isJustified := false
if !flag.NoShuffle {
rand.Shuffle(len(flag.Choice), func(i, j int) {
flag.Choice[i], flag.Choice[j] = flag.Choice[j], flag.Choice[i]
})
}
for cid, choice := range flag.Choice {
var val bool
var justify string
if choice.Value == nil {
val = false
} else if p, ok := choice.Value.(bool); ok {
val = p
if len(choice.Help) > 0 {
errs = append(errs, fmt.Sprintf("%q: error MCQ #%d: help field has to be used only with justified mcq.", path.Base(exercice.Path), nline + 1))
continue
}
if isJustified {
errs = append(errs, fmt.Sprintf("%q: error MCQ #%d: all true items has to be justified in this MCQ.", path.Base(exercice.Path), nline + 1))
continue
}
} else {
if choice.Raw == nil && choice.Value != nil {
choice.Raw = choice.Value
choice.Value = nil
}
if len(choice.Help) == 0 {
choice.Help = "Flag correspondant"
}
if hasOne && !isJustified {
errs = append(errs, fmt.Sprintf("%q: error MCQ #%d: all true items has to be justified in this MCQ.", path.Base(exercice.Path), nline + 1))
continue
}
var prep string
var terrs []string
val = true
isJustified = true
justify, prep, terrs = getRawKey(choice)
if len(terrs) > 0 {
for _, err := range terrs {
errs = append(errs, fmt.Sprintf("%q: flag MCQ #%d, choice #%d: %s", path.Base(exercice.Path), nline + 1, cid, err))
}
continue
}
choice.Help = prep + choice.Help
}
if e, err := f.AddEntry(choice.Label, val); err != nil {
errs = append(errs, fmt.Sprintf("%q: error in MCQ %d choice %d: %s", path.Base(exercice.Path), nline + 1, cid, err))
continue
} else if len(justify) > 0 {
if _, err := exercice.AddRawFlagKey(fmt.Sprintf("%%%d%%%s", e.Id, choice.Help), "", false, nil, []byte(justify), 0); err != nil {
errs = append(errs, fmt.Sprintf("%q: error MCQ #%d: %s", path.Base(exercice.Path), nline + 1, err))
continue
}
}
if val {
hasOne = true
}
}
if !hasOne {
errs = append(errs, fmt.Sprintf("%q: warning MCQ %d: no valid answer defined, is this really expected?", path.Base(exercice.Path), nline + 1))
}
}
}
if flag.Id != 0 {
kmap[flag.Id] = addedFlag
}
// Import dependency to flag
for _, nf := range flag.NeedFlag {
if rf, ok := kmap[nf.Id]; !ok {
errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to flag id=%d: id not defined, perhaps not available at time of processing", path.Base(exercice.Path), nline + 1, nf.Id))
continue
} else if err := addedFlag.AddDepend(rf); err != nil {
errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to id=%d: %s", path.Base(exercice.Path), nline + 1, nf.Id, err))
continue
}
}
// Import dependency to file
for _, lf := range flag.LockedFile {
if rf, err := exercice.GetFileByFilename(lf.Filename); err != nil {
errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to %s: %s", path.Base(exercice.Path), nline + 1, lf.Filename, err))
continue
} else if err := rf.AddDepend(addedFlag); err != nil {
errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to %s: %s", path.Base(exercice.Path), nline + 1, lf.Filename, err))
continue
}
}
}
}
return
}