intelligent_test_evaluator.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. package service
  2. import (
  3. "bytes"
  4. "encoding/xml"
  5. "errors"
  6. "io"
  7. "regexp"
  8. "strings"
  9. )
  10. type IntelligentTestEvaluation struct {
  11. Status string
  12. Score *float64
  13. Image string
  14. Detail map[string]any
  15. }
  16. type IntelligentTestEvaluator interface {
  17. Evaluate(string, IntelligentTestConfig) IntelligentTestEvaluation
  18. }
  19. // Evaluators are registered independently of test names. New test types can reuse them.
  20. func DefaultIntelligentTestEvaluators() map[string]IntelligentTestEvaluator {
  21. return map[string]IntelligentTestEvaluator{"svg_structure": svgStructureEvaluator{}, "exact_answer": exactAnswerEvaluator{}}
  22. }
  23. type svgStructureEvaluator struct{}
  24. func (svgStructureEvaluator) Evaluate(output string, _ IntelligentTestConfig) IntelligentTestEvaluation {
  25. return evaluateIntelligentSVG(output)
  26. }
  27. var answerLinePattern = regexp.MustCompile(`(?im)^[\t ]*ANSWER[\t ]*[::][\t ]*([^\r\n]+)[\t ]*$`)
  28. type exactAnswerEvaluator struct{}
  29. func (exactAnswerEvaluator) Evaluate(output string, cfg IntelligentTestConfig) IntelligentTestEvaluation {
  30. return evaluateIntelligentAnswer(output, cfg)
  31. }
  32. // SVG is never trusted HTML. Only a static SVG vocabulary is accepted, then re-encoded.
  33. // Returned SVG may be delivered as an image with CSP sandbox; never use v-html.
  34. func SanitizeIntelligentTestSVG(output string) (string, error) {
  35. start := strings.Index(output, "<svg")
  36. end := strings.LastIndex(output, "</svg>")
  37. if start < 0 || end < start {
  38. return "", errors.New("未返回完整 SVG")
  39. }
  40. raw := output[start : end+len("</svg>")]
  41. if len(raw) > 128*1024 {
  42. return "", errors.New("SVG 超过 128 KiB 上限")
  43. }
  44. elements := intelligentSVGElements
  45. attrs := intelligentSVGAttributes
  46. allowed := make(map[string]bool, len(elements))
  47. for _, v := range elements {
  48. allowed[v] = true
  49. }
  50. attrAllowed := make(map[string]bool, len(attrs))
  51. for _, v := range attrs {
  52. attrAllowed[v] = true
  53. }
  54. decoder := xml.NewDecoder(strings.NewReader(raw))
  55. var out bytes.Buffer
  56. encoder := xml.NewEncoder(&out)
  57. depth, shapes, nodes, roots := 0, 0, 0, 0
  58. for {
  59. tok, err := decoder.Token()
  60. if err == io.EOF {
  61. break
  62. }
  63. if err != nil {
  64. return "", errors.New("SVG XML 格式错误")
  65. }
  66. switch t := tok.(type) {
  67. case xml.StartElement:
  68. nodes++
  69. depth++
  70. if depth == 1 {
  71. roots++
  72. if roots > 1 {
  73. return "", errors.New("SVG 必须只有一个根节点")
  74. }
  75. }
  76. if depth > 40 || nodes > 6000 {
  77. return "", errors.New("SVG 结构过于复杂")
  78. }
  79. if !allowed[t.Name.Local] || (t.Name.Space != "" && t.Name.Space != "http://www.w3.org/2000/svg") {
  80. return "", errors.New("SVG 包含不允许的元素")
  81. }
  82. if depth == 1 && t.Name.Local != "svg" {
  83. return "", errors.New("SVG 根节点错误")
  84. }
  85. t.Name.Space = ""
  86. safe := []xml.Attr{}
  87. if depth == 1 {
  88. safe = append(safe, xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: "http://www.w3.org/2000/svg"})
  89. }
  90. for _, a := range t.Attr {
  91. if a.Name.Local == "xmlns" && a.Value == "http://www.w3.org/2000/svg" {
  92. continue
  93. }
  94. if a.Name.Space != "" || !attrAllowed[a.Name.Local] {
  95. return "", errors.New("SVG 包含活动内容或不允许的属性")
  96. }
  97. lower := strings.ToLower(a.Value)
  98. if strings.ContainsAny(a.Value, "<>\x00") || strings.Contains(lower, "javascript:") || strings.Contains(lower, "data:") || strings.Contains(lower, "http:") || strings.Contains(lower, "https:") || strings.Contains(lower, "//") {
  99. return "", errors.New("SVG 不允许外部资源")
  100. }
  101. if strings.Contains(lower, "url(") && !regexp.MustCompile(`^url\(#[A-Za-z_][A-Za-z0-9_.-]*\)$`).MatchString(strings.TrimSpace(a.Value)) {
  102. return "", errors.New("SVG 只允许本地图形引用")
  103. }
  104. safe = append(safe, a)
  105. }
  106. t.Attr = safe
  107. if strings.Contains(" path rect circle ellipse line polyline polygon ", " "+t.Name.Local+" ") {
  108. shapes++
  109. }
  110. if err := encoder.EncodeToken(t); err != nil {
  111. return "", err
  112. }
  113. case xml.EndElement:
  114. t.Name.Space = ""
  115. depth--
  116. if err := encoder.EncodeToken(t); err != nil {
  117. return "", err
  118. }
  119. case xml.CharData:
  120. if err := encoder.EncodeToken(t); err != nil {
  121. return "", err
  122. }
  123. case xml.Comment: // comments are unnecessary in the rendered artifact
  124. default:
  125. return "", errors.New("SVG 不允许指令或实体声明")
  126. }
  127. }
  128. if depth != 0 || shapes == 0 {
  129. return "", errors.New("SVG 未包含可绘制图形")
  130. }
  131. if err := encoder.Flush(); err != nil {
  132. return "", err
  133. }
  134. return out.String(), nil
  135. }