sync: Extract function that import flags from importer

This commit is contained in:
nemunaire 2019-07-05 22:28:56 +02:00
commit 4039a394b5
8 changed files with 438 additions and 294 deletions

View file

@ -1,9 +1,11 @@
package sync
import (
"fmt"
"path"
"github.com/BurntSushi/toml"
"srs.epita.fr/fic-server/libfic"
)
// ExerciceHintParams holds EHint definition infomation.
@ -35,15 +37,15 @@ type ExerciceFlagChoice struct {
// ExerciceFlag holds informations about one flag.
type ExerciceFlag struct {
Id int64
Label string `toml:",omitempty"`
Type string `toml:",omitempty"`
Label string `toml:",omitempty"`
Type string `toml:",omitempty"`
Raw interface{}
Separator string `toml:",omitempty"`
Ordered bool `toml:",omitempty"`
IgnoreCase bool `toml:",omitempty"`
ValidatorRe string `toml:"validator_regexp,omitempty"`
Help string `toml:",omitempty"`
ChoicesCost int64 `toml:"choices_cost,omitempty"`
Separator string `toml:",omitempty"`
Ordered bool `toml:",omitempty"`
IgnoreCase bool `toml:",omitempty"`
ValidatorRe string `toml:"validator_regexp,omitempty"`
Help string `toml:",omitempty"`
ChoicesCost int64 `toml:"choices_cost,omitempty"`
Choice []ExerciceFlagChoice
LockedFile []ExerciceUnlockFile `toml:"unlock_file,omitempty"`
NeedFlag []ExerciceDependency `toml:"need_flag,omitempty"`
@ -70,3 +72,45 @@ func parseExerciceParams(i Importer, exPath string) (p ExerciceParams, err error
return
}
// getExerciceParams returns normalized
func getExerciceParams(i Importer, exercice fic.Exercice) (params ExerciceParams, errs []string) {
var err error
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 {
// Treat legacy UCQ flags as ExerciceFlag
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,
})
}
params.FlagsUCQ = []ExerciceFlag{}
// Treat legacy MCQ flags as ExerciceFlag
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,
})
}
params.FlagsMCQ = []ExerciceFlag{}
}
return
}

View file

@ -82,234 +82,299 @@ func getRawKey(input interface{}, validatorRe string, ordered bool) (raw string,
return
}
// SyncExerciceFlags reads the content of challenge.txt and import "classic" flags as Key for the given challenge.
func buildKeyFlag(exercice fic.Exercice, flag ExerciceFlag, flagline int, defaultLabel string) (f *fic.Flag, choices []fic.FlagChoice, errs []string) {
if len(flag.Label) == 0 {
flag.Label = defaultLabel
}
if flag.Label[0] == '`' {
errs = append(errs, fmt.Sprintf("%q: flag #%d: Label should not begin with `.", path.Base(exercice.Path), flagline))
flag.Label = flag.Label[1:]
}
raw, prep, terrs := getRawKey(flag.Raw, flag.ValidatorRe, flag.Ordered)
if len(terrs) > 0 {
for _, err := range terrs {
errs = append(errs, fmt.Sprintf("%q: flag #%d: %s", path.Base(exercice.Path), flagline, err))
}
f = nil
return
}
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), flagline))
}
hashedFlag := fic.ComputeHashedFlag([]byte(raw))
fl := fic.Flag(fic.FlagKey{
IdExercice: exercice.Id,
Label: flag.Label,
Help: flag.Help,
IgnoreCase: flag.IgnoreCase,
ValidatorRegexp: validatorRegexp(flag.ValidatorRe),
Checksum: hashedFlag[:],
ChoicesCost: flag.ChoicesCost,
})
f = &fl
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 _, choice := range flag.Choice {
val, prep, terrs := getRawKey(choice.Value, "", false)
if len(terrs) > 0 {
for _, err := range terrs {
errs = append(errs, fmt.Sprintf("%q: flag #%d: %s", path.Base(exercice.Path), flagline, err))
}
continue
}
if len(choice.Label) == 0 {
choice.Label = val
}
choice.Label = prep + choice.Label
choices = append(choices, fic.FlagChoice{
Label: choice.Label,
Value: val,
})
if val == raw {
hasOne = true
}
}
if !hasOne {
errs = append(errs, fmt.Sprintf("%q: error in flag #%d: no valid answer defined.", path.Base(exercice.Path), flagline))
}
}
return
}
type importFlag struct {
Line int
Flag fic.Flag
Choices []fic.FlagChoice
FilesDeps []string
FlagsDeps []int64
}
// buildExerciceFlags read challenge.txt and extract all flags.
func buildExerciceFlags(i Importer, exercice fic.Exercice) (flags map[int64]importFlag, errs []string) {
params, gerrs := getExerciceParams(i, exercice)
if len(gerrs) > 0 {
return flags, gerrs
}
flags = map[int64]importFlag{}
for nline, flag := range params.Flags {
if flag.Id == 0 {
// TODO: should be more smart than that. Perhaps search to increment if possible.
flag.Id = rand.Int63()
}
// Ensure flag ID is unique
for _, ok := flags[flag.Id]; ok; _, ok = flags[flag.Id] {
errs = append(errs, fmt.Sprintf("%q: flag #%d: identifier already used (%d), using a random one.", path.Base(exercice.Path), nline+1, flag.Id))
flag.Id = rand.Int63()
}
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
}
if flag.Type == "key" || flag.Type == "ucq" {
addedFlag, choices, berrs := buildKeyFlag(exercice, flag, nline+1, "Flag")
if len(berrs) > 0 {
errs = append(errs, berrs...)
}
if addedFlag != nil {
flags[flag.Id] = importFlag{
Line: nline + 1,
Flag: *addedFlag,
Choices: choices,
}
}
} else if flag.Type == "mcq" {
addedFlag := fic.MCQ{
IdExercice: exercice.Id,
Title: flag.Label,
Entries: []fic.MCQ_entry{},
}
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
if choice.Justification != nil {
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
}
val = true
isJustified = true
} else if p, ok := choice.Value.(bool); ok {
val = p
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.Value == nil {
val = false
} else {
errs = append(errs, fmt.Sprintf("%q: error in MCQ %d choice %d: incorrect type for value: %T is not boolean.", path.Base(exercice.Path), nline+1, cid, choice.Value))
continue
}
addedFlag.Entries = append(addedFlag.Entries, fic.MCQ_entry{
Label: choice.Label,
Response: val,
})
if isJustified && choice.Justification != nil {
addedFlag, choices, berrs := buildKeyFlag(exercice, *choice.Justification, nline+1, "Flag correspondant")
if len(berrs) > 0 {
errs = append(errs, berrs...)
}
if addedFlag != nil {
flags[flag.Id] = importFlag{
Line: nline,
Flag: *addedFlag,
Choices: choices,
}
}
}
}
}
// Read dependency to flag
for _, nf := range flag.NeedFlag {
if v, ok := flags[flag.Id]; ok {
v.FlagsDeps = append(v.FlagsDeps, nf.Id)
flags[flag.Id] = v
}
}
// Read dependency to file
for _, lf := range flag.LockedFile {
if v, ok := flags[flag.Id]; ok {
v.FilesDeps = append(v.FilesDeps, lf.Filename)
flags[flag.Id] = v
}
}
}
return
}
// CheckExerciceFlags checks if all flags for the given challenge are correct.
func CheckExerciceFlags(i Importer, exercice fic.Exercice, files []fic.EFile) (rf []fic.Flag, errs []string) {
flags, berrs := buildExerciceFlags(i, exercice)
errs = append(errs, berrs...)
for _, flag := range flags {
// Check dependency to flag
for _, nf := range flag.FlagsDeps {
if _, ok := flags[nf]; !ok {
errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to flag id=%d: id not defined", path.Base(exercice.Path), flag.Line, nf))
}
}
// Check dependency to file
for _, lf := range flag.FilesDeps {
found := false
for _, f := range files {
if f.Name == lf {
found = true
break
}
}
if !found {
errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to %s: No such file", path.Base(exercice.Path), flag.Line, lf))
}
}
rf = append(rf, flag.Flag)
}
return
}
// SyncExerciceFlags imports all kind of flags 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 {
flags, berrs := buildExerciceFlags(i, exercice)
errs = append(errs, berrs...)
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.Raw, flag.ValidatorRe, flag.Ordered)
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.Value, "", false)
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
}
if len(choice.Label) == 0 {
choice.Label = val
}
choice.Label = prep + choice.Label
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))
// Import flags
for flagid, flag := range flags {
if addedFlag, err := exercice.AddFlag(flag.Flag); err != nil {
errs = append(errs, fmt.Sprintf("%q: error flag #%d: %s", path.Base(exercice.Path), flag.Line, err))
} else {
if f, ok := addedFlag.(fic.FlagKey); ok {
for _, choice := range flag.Choices {
if _, err := f.AddChoice(choice); err != nil {
errs = append(errs, fmt.Sprintf("%q: error in flag #%d choice #FIXME: %s", path.Base(exercice.Path), flag.Line, err))
}
}
}
} 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]
})
}
kmap[flagid] = addedFlag
for cid, choice := range flag.Choice {
var val bool
var justify string
if choice.Justification != nil {
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
}
val = true
isJustified = true
} else if p, ok := choice.Value.(bool); ok {
val = p
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.Value == nil {
val = false
} else {
errs = append(errs, fmt.Sprintf("%q: error in MCQ %d choice %d: incorrect type for value: %T is not boolean.", path.Base(exercice.Path), nline + 1, cid, choice.Value))
continue
}
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 isJustified && choice.Justification != nil {
var prep string
var terrs []string
justify, prep, terrs = getRawKey(choice.Justification.Raw, choice.Justification.ValidatorRe, choice.Justification.Ordered)
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
}
if len(choice.Justification.Label) == 0 {
choice.Justification.Label = "Flag correspondant"
}
choice.Justification.Label = prep + choice.Justification.Label
if _, err := exercice.AddRawFlagKey(fmt.Sprintf("%%%d%%%s", e.Id, choice.Justification.Label), choice.Justification.Help, choice.Justification.IgnoreCase, validatorRegexp(choice.Justification.ValidatorRe), []byte(justify), flag.ChoicesCost); 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))
// Import dependency to flag
for _, nf := range flag.FlagsDeps {
if rf, ok := kmap[nf]; !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), flag.Line, nf))
} 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), flag.Line, nf, err))
}
}
}
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
// Import dependency to file
for _, lf := range flag.FilesDeps {
if rf, err := exercice.GetFileByFilename(lf); err != nil {
errs = append(errs, fmt.Sprintf("%q: error flag #%d dependency to %s: %s", path.Base(exercice.Path), flag.Line, lf, err))
} 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), flag.Line, lf, err))
}
}
}
}
}
return
}