diff --git a/embed_dispatch_test.go b/embed_dispatch_test.go new file mode 100644 index 0000000..94908d6 --- /dev/null +++ b/embed_dispatch_test.go @@ -0,0 +1,48 @@ +package zas + +import ( + "reflect" + "strings" + "testing" + + "github.com/PuerkitoBio/goquery" +) + +// B6: config data must not be able to pick an arbitrary Generator method via +// reflection and panic method.Call with a mismatched arity/signature. + +func TestIsEmbedPluginMethod(t *testing.T) { + gen := &Generator{} + + if valid := reflect.ValueOf(gen).MethodByName("Markdown"); !isEmbedPluginMethod(valid) { + t.Fatal("isEmbedPluginMethod(Markdown) = false, want true") + } + // Run() error is a real exported Generator method, but with the wrong + // arity (0 args) for embed dispatch - must be rejected, not panic. + if wrongArity := reflect.ValueOf(gen).MethodByName("Run"); isEmbedPluginMethod(wrongArity) { + t.Fatal("isEmbedPluginMethod(Run) = true, want false") + } + if notFound := reflect.ValueOf(gen).MethodByName("DoesNotExist"); isEmbedPluginMethod(notFound) { + t.Fatal("isEmbedPluginMethod(missing method) = true, want false") + } +} + +func TestHandleEmbedTagsFallsBackOnWrongArityMethod(t *testing.T) { + // Reproduces the audit repro: mimetypes: {text/x-t: run} resolves to the + // real, exported Run() error method, which has the wrong signature for + // embed dispatch. Must fall back to the external plugin path instead of + // panicking in reflect.Value.Call. The fallback path itself is a no-op + // here (see audit A1, out of scope), so simply completing without a + // panic is the regression this guards. + gen := &Generator{ + Config: ConfigSection{ + "mimetypes": ConfigSection{"text/x-t": "run"}, + }, + } + doc, err := goquery.NewDocumentFromReader(strings.NewReader( + ``)) + if err != nil { + t.Fatal(err) + } + _ = gen.handleEmbedTags(doc, &ZasData{}) +} diff --git a/generate.go b/generate.go index 0e115fc..dab1ab8 100644 --- a/generate.go +++ b/generate.go @@ -576,7 +576,7 @@ func (gen *Generator) handleEmbedTags(doc *goquery.Document, data *ZasData) (err } plugin := gen.resolveMIMETypePlugin(typ) method := reflect.ValueOf(gen).MethodByName(cases.Title(language.English).String(plugin)) - if method == reflect.ValueOf(nil) { + if !isEmbedPluginMethod(method) { err = gen.handleMIMETypePlugin(e, doc) } else { args := make([]reflect.Value, 3) @@ -598,6 +598,34 @@ func (gen *Generator) handleEmbedTags(doc *goquery.Document, data *ZasData) (err return } +/* + * Reports whether method is a valid embed-plugin dispatch target: a method + * with the exact (e *goquery.Selection, doc *goquery.Document, data *ZasData) error + * signature. Config data chooses the method name (see resolveMIMETypePlugin), + * so any exported Generator method is reachable by MethodByName and must be + * shape-checked before Call to avoid a reflect panic on arity/type mismatch. + */ +func isEmbedPluginMethod(method reflect.Value) bool { + if !method.IsValid() { + return false + } + want := []reflect.Type{ + reflect.TypeFor[*goquery.Selection](), + reflect.TypeFor[*goquery.Document](), + reflect.TypeFor[*ZasData](), + } + t := method.Type() + if t.NumIn() != len(want) || t.NumOut() != 1 { + return false + } + for i, w := range want { + if t.In(i) != w { + return false + } + } + return true +} + type bufErr struct { buffer []byte err error