| 610 | explicit LuauEngine(BudgetLimits limits) : limits_(limits) {} |
| 611 | |
| 612 | Expected<std::vector<FilterClass>> inspectModule(const std::string& source, const std::string& origin) override { |
| 613 | BudgetState budget; // declared before the Closer → outlives lua_close |
| 614 | lua_State* L = newSandboxedState(&budget, limits_); |
| 615 | if (L == nullptr) { |
| 616 | return PJ::unexpected("failed to create Luau state"); |
| 617 | } |
| 618 | struct Closer { |
| 619 | lua_State* s; |
| 620 | ~Closer() { |
| 621 | lua_close(s); |
| 622 | } |
| 623 | } closer{L}; |
| 624 | |
| 625 | if (std::string err = loadModule(L, &budget, source); !err.empty()) { |
| 626 | return PJ::unexpected(err); |
| 627 | } |
| 628 | // Module value on top: either a single class table (has `create`) or a list. |
| 629 | const int module_idx = lua_gettop(L); |
| 630 | if (!lua_istable(L, module_idx)) { |
| 631 | return PJ::unexpected("a filter module must return a table"); |
| 632 | } |
| 633 | |
| 634 | std::vector<FilterClass> classes; |
| 635 | auto read_one = [&](int class_idx) -> std::string { |
| 636 | FilterClass fc; |
| 637 | fc.source = source; |
| 638 | fc.origin = origin; |
| 639 | lua_pushvalue(L, class_idx); // metadata reader works on top-of-stack |
| 640 | std::string err = readClassMetadata(L, fc); |
| 641 | lua_pop(L, 1); |
| 642 | if (!err.empty()) { |
| 643 | return err; |
| 644 | } |
| 645 | classes.push_back(std::move(fc)); |
| 646 | return {}; |
| 647 | }; |
| 648 | |
| 649 | lua_rawgetfield(L, module_idx, "create"); |
| 650 | const bool is_single_class = lua_isfunction(L, -1); |
| 651 | lua_pop(L, 1); |
| 652 | |
| 653 | if (is_single_class) { |
| 654 | if (std::string err = read_one(module_idx); !err.empty()) { |
| 655 | return PJ::unexpected(err); |
| 656 | } |
| 657 | } else { |
| 658 | const int count = arrayLen(L, module_idx); |
| 659 | if (count == 0) { |
| 660 | return PJ::unexpected("filter module returned neither a class nor a list of classes"); |
| 661 | } |
| 662 | for (int i = 1; i <= count; ++i) { |
| 663 | lua_rawgeti(L, module_idx, i); |
| 664 | const int entry = lua_gettop(L); |
| 665 | if (!lua_istable(L, entry)) { |
| 666 | lua_pop(L, 1); |
| 667 | return PJ::unexpected("filter list entry is not a table"); |
| 668 | } |
| 669 | std::string err = read_one(entry); |