package sync import ( "errors" "fmt" "math/rand" "path" "sort" "strings" "unicode" "github.com/julienschmidt/httprouter" "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(input interface{}, validatorRe string, ordered bool, showLines bool) (raw string, prep string, errs []string) { separator := "," // Concatenate array if f, ok := input.([]interface{}); ok { if len(validatorRe) > 0 { errs = append(errs, "ValidatorRe cannot be defined for this kind of flag.") validatorRe = "" } if len(separator) == 0 { separator = "," } else if len(separator) > 1 { separator = string(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, 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 := "f" if !ordered { sort.Strings(fitems) ignord = "t" } nbLines := 0 if showLines { if len(fitems) > 9 { errs = append(errs, "too much items in vector to use ShowLines features, max 9.") } else { nbLines = len(fitems) } } raw = strings.Join(fitems, separator) + separator prep = fmt.Sprintf("`%s%s%d", separator, ignord, nbLines) } else if f, ok := input.(int64); ok { raw = fmt.Sprintf("%d", f) } else if f, ok := input.(string); !ok { errs = append(errs, fmt.Sprintf("has an invalid type: can only be []string or string, not %T", input)) return } else { raw = f } return } 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:] } 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.Sprintf("%q: flag #%d: Label should not end with punct (%q). Reword your label as a description of the expected flag, `:` are automatically appended.", path.Base(exercice.Path), flagline, flag.Label[len(flag.Label)-1])) } raw, prep, terrs := getRawKey(flag.Raw, flag.ValidatorRe, flag.Ordered, flag.ShowLines) 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 (flag.Type == "text" && !isFullGraphic(strings.Replace(raw, "\n", "", -1))) || (flag.Type != "text" && !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, err := fic.ComputeHashedFlag([]byte(raw), !flag.CaseSensitive, validatorRegexp(flag.ValidatorRe)) if err != nil { errs = append(errs, err.Error()) return } fl := fic.Flag(fic.FlagKey{ IdExercice: exercice.Id, Label: flag.Label, Help: flag.Help, IgnoreCase: !flag.CaseSensitive, Multiline: flag.Type == "text", 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, 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 buildExerciceFlag(i Importer, exercice fic.Exercice, flag ExerciceFlag, nline int) (ret []importFlag, errs []string) { switch strings.ToLower(flag.Type) { case "": flag.Type = "key" case "key": flag.Type = "key" case "text": flag.Type = "text" case "vector": flag.Type = "vector" 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', 'text', 'mcq', 'ucq' or 'vector'.", path.Base(exercice.Path), nline+1)) return } if flag.Type == "key" || flag.Type == "text" || flag.Type == "ucq" || flag.Type == "vector" { addedFlag, choices, berrs := buildKeyFlag(exercice, flag, nline+1, "Flag") if len(berrs) > 0 { errs = append(errs, berrs...) } if addedFlag != nil { ret = append(ret, 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.Raw != 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.Raw != nil { addedFlag, choices, berrs := buildKeyFlag(exercice, choice.ExerciceFlag, nline+1, "Flag correspondant") if len(berrs) > 0 { errs = append(errs, berrs...) } if addedFlag != nil { ret = append(ret, importFlag{ Line: nline + 1, Flag: *addedFlag, Choices: choices, }) } } } ret = append(ret, importFlag{ Line: nline + 1, Flag: addedFlag, }) } return } // buildExerciceFlags read challenge.txt and extract all flags. func buildExerciceFlags(i Importer, exercice fic.Exercice) (flags map[int64]importFlag, flagids []int64, errs []string) { params, gerrs := getExerciceParams(i, exercice) if len(gerrs) > 0 { return flags, flagids, 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() } newFlags, ferrs := buildExerciceFlag(i, exercice, flag, nline) if len(ferrs) > 0 { errs = append(errs, ferrs...) } if len(newFlags) > 0 { for _, newFlag := range newFlags { fId := flag.Id for _, ok := flags[fId]; ok; _, ok = flags[fId] { fId = rand.Int63() } // Read dependency to flag for _, nf := range flag.NeedFlag { newFlag.FlagsDeps = append(newFlag.FlagsDeps, nf.Id) } // Read dependency to file for _, lf := range flag.LockedFile { newFlag.FilesDeps = append(newFlag.FilesDeps, lf.Filename) } flags[fId] = newFlag flagids = append(flagids, fId) } } } return } // CheckExerciceFlags checks if all flags for the given challenge are correct. func CheckExerciceFlags(i Importer, exercice fic.Exercice, files []string) (rf []fic.Flag, errs []string) { flags, flagsids, berrs := buildExerciceFlags(i, exercice) errs = append(errs, berrs...) for _, flagid := range flagsids { if flag, ok := flags[flagid]; ok { // 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 == 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 } // ExerciceFlagsMap builds the flags bindings between challenge.txt and DB. func ExerciceFlagsMap(i Importer, exercice fic.Exercice) (kmap map[int64]fic.Flag) { flags, flagids, _ := buildExerciceFlags(i, exercice) kmap = map[int64]fic.Flag{} for _, flagid := range flagids { if flag, ok := flags[flagid]; ok { if addedFlag, err := flag.Flag.RecoverId(); err == nil { kmap[flagid] = addedFlag } } } return } // SyncExerciceFlags imports all kind of flags for the given challenge. func SyncExerciceFlags(i Importer, exercice fic.Exercice) (kmap map[int64]fic.Flag, 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 { flags, flagids, berrs := buildExerciceFlags(i, exercice) errs = append(errs, berrs...) kmap = map[int64]fic.Flag{} // Import flags for _, flagid := range flagids { if flag, ok := flags[flagid]; ok { 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)) } } } kmap[flagid] = addedFlag // 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)) } } // 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 } // ApiListRemoteExerciceFlags is an accessor letting foreign packages to access remote exercice flags. func ApiGetRemoteExerciceFlags(ps httprouter.Params, _ []byte) (interface{}, error) { theme, errs := BuildTheme(GlobalImporter, ps.ByName("thid")) if theme != nil { exercice, _, _, _, errs := BuildExercice(GlobalImporter, *theme, path.Join(theme.Path, ps.ByName("exid")), nil) if exercice != nil { flags, errs := CheckExerciceFlags(GlobalImporter, *exercice, []string{}) if flags != nil { return flags, nil } else { return flags, errors.New(fmt.Sprintf("%q", errs)) } } else { return exercice, errors.New(fmt.Sprintf("%q", errs)) } } else { return nil, errors.New(fmt.Sprintf("%q", errs)) } }