diff --git a/automation/tests/aegisub.cpp b/automation/tests/aegisub.cpp index f89d83249d..96ba1853e4 100644 --- a/automation/tests/aegisub.cpp +++ b/automation/tests/aegisub.cpp @@ -60,7 +60,7 @@ int main(int argc, char **argv) { } preload_modules(L); - Install(L, {"include"}); + Install(L, {"include"}, "include"); // Patch os.exit to close the lua state first since busted calls it when // it's done @@ -93,4 +93,3 @@ int main(int argc, char **argv) { check(L, lua_pcall(L, argc - 2, LUA_MULTRET, base)); lua_close(L); } - diff --git a/libaegisub/common/file_mapping.cpp b/libaegisub/common/file_mapping.cpp index a9d1b43046..00b1c41eaf 100644 --- a/libaegisub/common/file_mapping.cpp +++ b/libaegisub/common/file_mapping.cpp @@ -34,19 +34,30 @@ char *map(int64_t s_offset, uint64_t length, boost::interprocess::mode_t mode, std::unique_ptr& region, uint64_t& mapping_start) { static char dummy = 0; - if (length == 0) return &dummy; + if (s_offset < 0) + throw agi::InternalError("Attempted to map a negative file offset"); auto offset = static_cast(s_offset); - if (offset + length > file_size) + if (offset > file_size || length > file_size - offset) throw agi::InternalError("Attempted to map beyond end of file"); + if (length == 0) return &dummy; // Check if we can just use the current mapping - if (region && offset >= mapping_start && offset + length <= mapping_start + region->get_size()) - return static_cast(region->get_address()) + offset - mapping_start; + if (region && offset >= mapping_start) { + auto relative_offset = offset - mapping_start; + auto region_size = region->get_size(); + if (relative_offset <= region_size && length <= region_size - relative_offset) + return static_cast(region->get_address()) + relative_offset; + } if (sizeof(size_t) == 4) { mapping_start = offset & ~0xFFFFFULL; // Align to 1 MB boundary - length += static_cast(offset - mapping_start); + auto prefix = offset - mapping_start; + if (length > std::numeric_limits::max() - prefix) + throw std::bad_alloc(); + length += prefix; + if (length > std::numeric_limits::max() - 0xFFFFF) + throw std::bad_alloc(); // Map 16 MB or length rounded up to the next MB length = std::min(std::max(0x1000000U, (length + 0xFFFFF) & ~0xFFFFF), file_size - mapping_start); } diff --git a/libaegisub/common/vfr.cpp b/libaegisub/common/vfr.cpp index d08e549348..0f551f421f 100644 --- a/libaegisub/common/vfr.cpp +++ b/libaegisub/common/vfr.cpp @@ -21,6 +21,7 @@ #include "libaegisub/charset.h" #include "libaegisub/io.h" #include "libaegisub/line_iterator.h" +#include "libaegisub/util.h" #include #include @@ -28,12 +29,28 @@ #include #include #include +#include namespace { static const int64_t default_denominator = 1000000000; +static const size_t max_timecodes = 10000000; using agi::line_iterator; using namespace agi::vfr; +int64_t checked_fps_numerator(double fps) { + if (!std::isfinite(fps) || fps < 0.) + throw InvalidFramerate("FPS must be greater than zero"); + if (fps > 1000.) + throw InvalidFramerate("FPS must not be greater than 1000"); + return static_cast(fps * default_denominator); +} + +void append_v1_timecode(std::vector& timecodes, double time) { + if (!std::isfinite(time) || time < 0 || time > std::numeric_limits::max() - .5) + throw InvalidFramerate("V1 timecode exceeds the supported timestamp range"); + timecodes.push_back(static_cast(time + .5)); +} + /// @brief Verify that timecodes monotonically increase /// @param timecodes List of timecodes to check void validate_timecodes(std::vector const& timecodes) { @@ -76,7 +93,9 @@ TimecodeRange v1_parse_line(std::string const& str) { throw InvalidFramerate("Cannot specify frame rate for negative frames."); if (range.end < range.start) throw InvalidFramerate("End frame must be greater than or equal to start frame"); - if (range.fps <= 0.) + if (range.end > static_cast(max_timecodes) - 2) + throw InvalidFramerate("V1 timecode range exceeds the 10000000 frame limit"); + if (!std::isfinite(range.fps) || range.fps <= 0.) throw InvalidFramerate("FPS must be greater than zero"); if (range.fps > 1000.) // This is our limitation, not mkvmerge's @@ -92,8 +111,14 @@ TimecodeRange v1_parse_line(std::string const& str) { /// @param[out] last Unrounded time of the last frame /// @return Assumed fps times one million int64_t v1_parse(line_iterator file, std::string line, std::vector &timecodes, int64_t &last) { - double fps = atof(line.substr(7).c_str()); - if (fps <= 0.) throw InvalidFramerate("Assumed FPS must be greater than zero"); + auto fps_string = line.substr(7); + auto first = fps_string.find_first_not_of(" \t\r\n\f\v"); + auto last_char = fps_string.find_last_not_of(" \t\r\n\f\v"); + if (first != std::string::npos) + fps_string = fps_string.substr(first, last_char - first + 1); + double fps; + if (!agi::util::try_parse(fps_string, &fps) || !std::isfinite(fps) || fps <= 0.) + throw InvalidFramerate("Assumed FPS must be greater than zero"); if (fps > 1000.) throw InvalidFramerate("Assumed FPS must not be greater than 1000"); std::vector ranges; @@ -116,16 +141,19 @@ int64_t v1_parse(line_iterator file, std::string line, std::vector< throw InvalidFramerate("Override ranges must not overlap"); } for (; frame < range.start; ++frame) { - timecodes.push_back(int(time + .5)); + append_v1_timecode(timecodes, time); time += 1000. / fps; } for (; frame <= range.end; ++frame) { - timecodes.push_back(int(time + .5)); + append_v1_timecode(timecodes, time); time += 1000. / range.fps; } } - timecodes.push_back(int(time + .5)); - last = int64_t(time * fps * default_denominator); + append_v1_timecode(timecodes, time); + auto last_time = time * fps * default_denominator; + if (!std::isfinite(last_time) || last_time >= std::ldexp(1.0, 63)) + throw InvalidFramerate("V1 timecode exceeds the supported timestamp range"); + last = static_cast(last_time); return int64_t(fps * default_denominator); } } @@ -133,10 +161,8 @@ int64_t v1_parse(line_iterator file, std::string line, std::vector< namespace agi::vfr { Framerate::Framerate(double fps) : denominator(default_denominator) -, numerator(int64_t(fps * denominator)) +, numerator(checked_fps_numerator(fps)) { - if (fps < 0.) throw InvalidFramerate("FPS must be greater than zero"); - if (fps > 1000.) throw InvalidFramerate("FPS must not be greater than 1000"); timecodes.push_back(0); } @@ -178,7 +204,11 @@ Framerate::Framerate(agi::fs::path const& filename) auto encoding = agi::charset::Detect(filename); auto line = *line_iterator(*file, encoding.c_str()); if (line == "# timecode format v2") { - copy(line_iterator(*file, encoding.c_str()), line_iterator(), back_inserter(timecodes)); + for (auto timecode : line_iterator(*file, encoding.c_str())) { + if (timecodes.size() == max_timecodes) + throw InvalidFramerate("Timecode file exceeds the 10000000 entry limit"); + timecodes.push_back(timecode); + } SetFromTimecodes(); return; } diff --git a/libaegisub/include/libaegisub/line_iterator.h b/libaegisub/include/libaegisub/line_iterator.h index 1910f3961a..22e548d351 100644 --- a/libaegisub/include/libaegisub/line_iterator.h +++ b/libaegisub/include/libaegisub/line_iterator.h @@ -109,10 +109,11 @@ line_iterator end(line_iterator&) { return agi::line_iterator(); } template void line_iterator::next() { std::string str; - if (!getline(str)) - return; - if (!convert(str)) - next(); + do { + str.clear(); + if (!getline(str)) + return; + } while (!convert(str)); } template<> diff --git a/libaegisub/include/libaegisub/lua/script_reader.h b/libaegisub/include/libaegisub/lua/script_reader.h index 760d440256..a4fc8c02c9 100644 --- a/libaegisub/include/libaegisub/lua/script_reader.h +++ b/libaegisub/include/libaegisub/lua/script_reader.h @@ -23,6 +23,8 @@ namespace agi::lua { /// Load a Lua or Moonscript file at the given path bool LoadFile(lua_State *L, agi::fs::path const& filename); /// Install our module loader and add include_path to the module search - /// path of the given lua state - bool Install(lua_State *L, std::vector const& include_path); + /// path of the given lua state. Mandatory runtime support is loaded only + /// from support_path. + bool Install(lua_State *L, std::vector const& include_path, + agi::fs::path const& support_path); } diff --git a/libaegisub/lua/script_reader.cpp b/libaegisub/lua/script_reader.cpp index 77f1578055..56dabfa70a 100644 --- a/libaegisub/lua/script_reader.cpp +++ b/libaegisub/lua/script_reader.cpp @@ -25,6 +25,18 @@ #include namespace agi::lua { + namespace { + bool load_support_file(lua_State *L, fs::path const& filename) { + try { + return LoadFile(L, filename); + } + catch (agi::Exception const& e) { + lua_pushstring(L, e.GetMessage().c_str()); + return false; + } + } + } + bool LoadFile(lua_State *L, agi::fs::path const& raw_filename) { auto filename = raw_filename; try { @@ -119,7 +131,8 @@ namespace agi::lua { return lua_gettop(L) - pretop; } - bool Install(lua_State *L, std::vector const& include_path) { + bool Install(lua_State *L, std::vector const& include_path, + fs::path const& support_path) { // set the module load path to include_path lua_getglobal(L, "package"); push_value(L, "path"); @@ -148,18 +161,42 @@ namespace agi::lua { #ifdef _WIN32 // Replace the default lua IO functions with our unicode compatible ones - luaL_loadstring(L, "require('unicode-monkeypatch')"); + if (!load_support_file(L, support_path / "unicode-monkeypatch.lua")) + return false; if (lua_pcall(L, 0, 0, 0)) { return false; // leave error message } #endif - luaL_loadstring(L, "return require('moonscript').loadstring"); + // Load mandatory support by exact path. In particular, do not allow a + // moonscript.lua beside an approved subtitle-local script to run first. + if (!load_support_file(L, support_path / "moonscript.lua")) + return false; if (lua_pcall(L, 0, 1, 0)) { return false; // leave error message } + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + lua_pushliteral(L, "Bundled moonscript.lua did not return a module table"); + return false; + } + + lua_getfield(L, -1, "loadstring"); + if (!lua_isfunction(L, -1)) { + lua_pop(L, 2); + lua_pushliteral(L, "Bundled moonscript.lua has no loadstring function"); + return false; + } lua_setfield(L, LUA_REGISTRYINDEX, "moonscript"); + // Direct loading bypasses require(), so record the trusted module as + // loaded before user code gets control. + lua_getglobal(L, "package"); + lua_getfield(L, -1, "loaded"); + lua_pushvalue(L, -3); + lua_setfield(L, -2, "moonscript"); + lua_pop(L, 3); // loaded, package, module + return true; } } diff --git a/src/ass_attachment.cpp b/src/ass_attachment.cpp index 21c4a4c482..2fc43ed930 100644 --- a/src/ass_attachment.cpp +++ b/src/ass_attachment.cpp @@ -23,6 +23,8 @@ #include +#include + // Out-of-line to anchor vtable AssEntryGroup AssAttachment::Group() const { return group; } @@ -48,15 +50,23 @@ AssAttachment::AssAttachment(agi::fs::path const& name, AssEntryGroup group) agi::ass::UUEncode(buff, buff + file.size())); } +void AssAttachment::SetEntryData(std::string data) { + entry_data = std::move(data); +} + size_t AssAttachment::GetSize() const { - auto header_end = entry_data.get().find('\n'); - return entry_data.get().size() - header_end - 1; + auto const& data = entry_data.get(); + auto header_end = data.find('\n'); + return data.size() - header_end - 1; } void AssAttachment::Extract(agi::fs::path const& filename) const { - auto header_end = entry_data.get().find('\n'); - auto decoded = agi::ass::UUDecode(entry_data.get().c_str() + header_end + 1, &entry_data.get().back() + 1); - agi::io::Save(filename, true).Get().write(&decoded[0], decoded.size()); + auto const& data = entry_data.get(); + auto header_end = data.find('\n'); + auto decoded = agi::ass::UUDecode(data.c_str() + header_end + 1, &data.back() + 1); + agi::io::Save save(filename, true); + if (!decoded.empty()) + save.Get().write(decoded.data(), decoded.size()); } std::string AssAttachment::GetFileName(bool raw) const { diff --git a/src/ass_attachment.h b/src/ass_attachment.h index 67ec2641b9..2a7b3f287c 100644 --- a/src/ass_attachment.h +++ b/src/ass_attachment.h @@ -33,8 +33,8 @@ class AssAttachment final : public AssEntry { /// Get the size of the attached file in bytes size_t GetSize() const; - /// Add a line of data (without newline) read from a subtitle file - void AddData(std::string const& data) { entry_data = entry_data.get() + data + "\r\n"; } + /// Store entry data accumulated by the subtitle parser. + void SetEntryData(std::string data); /// Extract the contents of this attachment to a file /// @param filename Path to save the attachment to diff --git a/src/ass_override.cpp b/src/ass_override.cpp index 7e1fe59261..e4288815f3 100644 --- a/src/ass_override.cpp +++ b/src/ass_override.cpp @@ -362,7 +362,12 @@ void parse_parameters(AssOverrideTag *tag, std::string_view text, AssOverrideTag std::vector paramList = tokenize(text); size_t totalPars = paramList.size(); - int parsFlag = 1 << (totalPars - 1); // Get optional parameters flag + // Optional-parameter masks only describe the supported 1-8 parameter + // forms. Avoid shifting by a negative or oversized count for malformed + // tags with no parameters or far too many of them. + unsigned parsFlag = totalPars > 0 && totalPars <= 8 + ? 1u << (totalPars - 1) + : 0; // vector (i)clip is the second clip proto_ittype in the list if ((tag->Name == "\\clip" || tag->Name == "\\iclip") && totalPars != 4) { ++proto_it; diff --git a/src/ass_parser.cpp b/src/ass_parser.cpp index 2c3df23f6a..718e99fc2b 100644 --- a/src/ass_parser.cpp +++ b/src/ass_parser.cpp @@ -103,6 +103,12 @@ AssParser::AssParser(AssFile *target, int version) AssParser::~AssParser() = default; +void AssParser::FinishAttachment() { + attach->SetEntryData(std::move(attachment_data)); + target->Attachments.push_back(std::move(*attach)); + attach.reset(); +} + void AssParser::ParseAttachmentLine(std::string const& data) { bool is_filename = data.starts_with("fontname: ") || data.starts_with("filename: "); @@ -116,19 +122,21 @@ void AssParser::ParseAttachmentLine(std::string const& data) { // Data is over, add attachment to the file if (!valid_data || is_filename) { - target->Attachments.push_back(*attach.release()); + FinishAttachment(); AddLine(data); } else { - attach->AddData(data); + attachment_data.append(data).append("\r\n"); // Done building if (data.size() < 80) - target->Attachments.push_back(*attach.release()); + FinishAttachment(); } } -void AssParser::ParseScriptInfoLine(std::string const& data) { +void AssParser::ParseScriptInfoLine(std::string const& rawdata) { + std::string data = SanitizeLine(rawdata); + if (data.starts_with(";")) { // Skip stupid comments added by other programs // Of course, we'll add our own in place later... ;) @@ -187,13 +195,17 @@ void AssParser::ParseStyleLine(std::string const& data) { } void AssParser::ParseFontLine(std::string const& data) { - if (data.starts_with("fontname: ")) + if (data.starts_with("fontname: ")) { attach = std::make_unique(data, AssEntryGroup::FONT); + attachment_data = data + "\r\n"; + } } void AssParser::ParseGraphicsLine(std::string const& data) { - if (data.starts_with("filename: ")) + if (data.starts_with("filename: ")) { attach = std::make_unique(data, AssEntryGroup::GRAPHIC); + attachment_data = data + "\r\n"; + } } void AssParser::ParseExtradataLine(std::string const &rawdata) { diff --git a/src/ass_parser.h b/src/ass_parser.h index 243701721a..64abaa8411 100644 --- a/src/ass_parser.h +++ b/src/ass_parser.h @@ -25,8 +25,10 @@ class AssParser { AssFile *target; int version; std::unique_ptr attach; + std::string attachment_data; void (AssParser::*state)(std::string const&); + void FinishAttachment(); void ParseAttachmentLine(std::string const& data); void ParseEventLine(std::string const& data); void ParseStyleLine(std::string const& data); diff --git a/src/auto4_base.cpp b/src/auto4_base.cpp index 74254bb55a..b6ae0f41fe 100644 --- a/src/auto4_base.cpp +++ b/src/auto4_base.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -372,8 +373,8 @@ namespace Automation4 { auto const& local_scripts = context->ass->Properties.automation_scripts; - // Tuples of (raw script path, base path, resolved path) - using PathTuple = std::tuple; + // Tuples of (raw script path, base path, resolved path, canonical path) + using PathTuple = std::tuple; std::vector resolvedpaths; auto autobasefn(OPT_GET("Path/Automation/Base")->GetString()); @@ -397,7 +398,7 @@ namespace Automation4 { } auto sfname = basepath/trimmed; - resolvedpaths.emplace_back(tok, basepath, sfname); + resolvedpaths.emplace_back(tok, basepath, sfname, agi::fs::path()); } if (resolvedpaths.empty()) { @@ -430,15 +431,29 @@ namespace Automation4 { return; } - for (auto const& [tok, basepath, sfname] : resolvedpaths) { - if (agi::fs::FileExists(sfname)) - scripts.emplace_back(Automation4::ScriptFactory::CreateFromFile(sfname, true)); - else { + for (auto& [tok, basepath, sfname, canonical] : resolvedpaths) { + try { + canonical = agi::fs::Canonicalize(sfname); + if (!agi::fs::FileExists(canonical)) { + wxLogWarning(fmt_tl("Automation Script reference is not a file.\nFilename specified: %s\nResolved filename: %s", + to_wx(tok), canonical.wstring())); + canonical.clear(); + continue; + } + } + catch (agi::Exception const& e) { + canonical.clear(); wxLogWarning(fmt_tl("Automation Script referenced could not be found.\nFilename specified: %s\nSearched relative to: %s\nResolved filename: %s", to_wx(tok), basepath.wstring(), sfname.wstring())); + LOG_W("auto4") << "Could not canonicalize local script: " << e.GetMessage(); } } + for (auto const& [tok, basepath, sfname, canonical] : resolvedpaths) { + if (!canonical.empty() && agi::fs::FileExists(canonical)) + scripts.emplace_back(Automation4::ScriptFactory::CreateFromFile(canonical, true)); + } + ScriptsChanged(); } diff --git a/src/auto4_lua.cpp b/src/auto4_lua.cpp index fe5b18f0c8..c2680b7b98 100644 --- a/src/auto4_lua.cpp +++ b/src/auto4_lua.cpp @@ -483,7 +483,7 @@ namespace { // Replace the default lua module loader with our unicode compatible // one and set the module search path - if (!Install(L, include_path)) { + if (!Install(L, include_path, config::path->Decode("?data/automation/include/"))) { description = get_string_or_default(L, 1); lua_pop(L, 1); return; diff --git a/src/avisynth_wrap.cpp b/src/avisynth_wrap.cpp index d76a377d28..059c6b2cb1 100644 --- a/src/avisynth_wrap.cpp +++ b/src/avisynth_wrap.cpp @@ -51,11 +51,17 @@ namespace { typedef IScriptEnvironment* __stdcall FUNC(int); AviSynthWrapper::AviSynthWrapper() { - if (!avs_refcount++) { - hLib = LoadLibrary(L"avisynth.dll"); + if (avs_refcount) { + ++avs_refcount; + return; + } + + try { + hLib = LoadLibraryExW(L"avisynth.dll", nullptr, + LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); if (!hLib) - throw AvisynthError("Could not load avisynth.dll"); + throw AvisynthError("Could not load avisynth.dll from the application or Windows system directory"); FUNC *CreateScriptEnv = (FUNC*)GetProcAddress(hLib, "CreateScriptEnvironment"); if (!CreateScriptEnv) @@ -74,13 +80,25 @@ AviSynthWrapper::AviSynthWrapper() { const int memoryMax = OPT_GET("Provider/Avisynth/Memory Max")->GetInt(); if (memoryMax) env->SetMemoryMax(memoryMax); + + avs_refcount = 1; + } + catch (...) { + delete env; + env = nullptr; + if (hLib) + FreeLibrary(hLib); + hLib = nullptr; + throw; } } AviSynthWrapper::~AviSynthWrapper() { if (!--avs_refcount) { delete env; + env = nullptr; FreeLibrary(hLib); + hLib = nullptr; } } diff --git a/src/dialog_attachments.cpp b/src/dialog_attachments.cpp index 00e7572e96..8ab38a06f4 100644 --- a/src/dialog_attachments.cpp +++ b/src/dialog_attachments.cpp @@ -30,19 +30,57 @@ #include "ass_attachment.h" #include "ass_file.h" #include "compat.h" +#include "format.h" #include "help_button.h" #include "libresrc/libresrc.h" #include "options.h" #include "utils.h" +#include +#include + #include #include #include #include #include +#include #include namespace { +bool is_safe_attachment_filename(std::string const& filename) { + if (filename.empty() || filename == "." || filename == "..") + return false; + if (filename.find_first_of("/\\:") != std::string::npos) + return false; + if (std::any_of(filename.begin(), filename.end(), [](unsigned char c) { return c < 0x20; })) + return false; + +#ifdef _WIN32 + if (filename.back() == ' ' || filename.back() == '.') + return false; + + auto basename = filename.substr(0, filename.find('.')); + for (auto& c : basename) { + if (c >= 'a' && c <= 'z') + c -= 'a' - 'A'; + } + if (basename == "CON" || basename == "PRN" || basename == "AUX" || basename == "NUL" || basename == "CLOCK$") + return false; + if (basename.size() == 4 && (basename.starts_with("COM") || basename.starts_with("LPT")) && basename[3] >= '1' && basename[3] <= '9') + return false; +#endif + + return true; +} + +std::optional attachment_destination(agi::fs::path const& directory, std::string const& filename) { + if (!is_safe_attachment_filename(filename)) + return {}; + + return directory / agi::fs::path(filename); +} + struct DialogAttachments { wxDialog d; AssFile *ass; @@ -163,10 +201,13 @@ void DialogAttachments::OnExtract(wxCommandEvent &) { if (listView->GetNextSelected(i) != -1) path = wxDirSelector(_("Select the path to save the files to:"), to_wx(OPT_GET("Path/Fonts Collector Destination")->GetString())).utf8_str().data(); else { + auto default_filename = ass->Attachments[i].GetFileName(); + if (!is_safe_attachment_filename(default_filename)) + default_filename = "attachment"; path = SaveFileSelector( _("Select the path to save the file to:"), "Path/Fonts Collector Destination", - ass->Attachments[i].GetFileName(), + default_filename, "", from_wx(_("All Supported Formats") + " (*.bmp, *.gif, *.jpg, *.ico, *.ttf, *.wmf)|*.bmp;*.gif;*.jpg;*.ico;*.ttf;*.wmf|" + _("Font Files") + " (*.ttf)|*.ttf|" + _("Graphic Files") + " (*.bmp, *.gif, *.jpg, *.ico, *.wmf)|*.bmp;*.gif;*.jpg;*.ico;*.wmf"), &d); fullPath = true; @@ -176,8 +217,24 @@ void DialogAttachments::OnExtract(wxCommandEvent &) { // Loop through items in list while (i != -1) { auto& attach = ass->Attachments[i]; - attach.Extract(fullPath ? path : path/attach.GetFileName()); - i = listView->GetNextSelected(i); + auto next = listView->GetNextSelected(i); + auto destination = fullPath ? std::optional(path) : attachment_destination(path, attach.GetFileName()); + if (!destination) { + wxMessageBox( + fmt_tl("The attachment \"%s\" was not extracted because its filename is unsafe.", to_wx(attach.GetFileName(true))), + _("Attachment not extracted"), wxOK | wxICON_WARNING | wxCENTRE, &d); + i = next; + continue; + } + if (!fullPath && agi::fs::Exists(*destination) && + wxMessageBox( + fmt_tl("The file \"%s\" already exists. Do you want to replace it?", destination->wstring()), + _("Replace existing file?"), wxYES_NO | wxNO_DEFAULT | wxICON_WARNING | wxCENTRE, &d) != wxYES) { + i = next; + continue; + } + attach.Extract(*destination); + i = next; } } diff --git a/src/font_file_lister_coretext.mm b/src/font_file_lister_coretext.mm index a9bb6235d8..ce87e69a22 100644 --- a/src/font_file_lister_coretext.mm +++ b/src/font_file_lister_coretext.mm @@ -39,6 +39,10 @@ FontMatch process_descriptor(NSFontDescriptor *desc, NSString *name) { return ret; NSFont *font = [NSFont fontWithDescriptor:desc size:10]; + if (!font) { + ret.url = nil; + return ret; + } // Ask CoreText if the font is italic, but if it says no double-check // by reading the macStyle field of the 'head' table as CT doesn't honor @@ -79,27 +83,39 @@ FontMatch process_descriptor(NSFontDescriptor *desc, NSString *name) { // For VSFilter compatibility we want to match based on the Windows name. if (ret.family_match) { auto data = (__bridge_transfer NSData *)CTFontCopyTable((__bridge CTFontRef)font, kCTFontTableName, 0); + if (data.length < 6) + return ret; + auto bytes = static_cast(data.bytes); + auto table_size = static_cast(data.length); uint16_t count = get_16(bytes, 2); - auto strings = bytes + get_16(bytes, 4); + size_t strings_offset = get_16(bytes, 4); + if (count > (table_size - 6) / 12 || strings_offset > table_size) + return ret; + auto strings = bytes + strings_offset; + auto strings_size = table_size - strings_offset; for (uint16_t i = 0; i < count; ++i) { auto name_record = bytes + 6 + i * 12; auto platform_id = get_16(name_record, 0); auto encoding_id = get_16(name_record, 2); auto name_id = get_16(name_record, 6); - auto length = get_16(name_record, 8); - auto offset = get_16(name_record, 10); + size_t length = get_16(name_record, 8); + size_t offset = get_16(name_record, 10); if (name_id != 1) // font family continue; if (platform_id != 3 || encoding_id != 1) // only look at MS Unicode continue; + if ((length & 1) || offset > strings_size || length > strings_size - offset) + continue; NSString *msFamily = [[NSString alloc] initWithBytesNoCopy:(void *)(strings + offset) length:length encoding:NSUTF16BigEndianStringEncoding freeWhenDone:NO]; + if (!msFamily) + continue; auto range = [msFamily rangeOfString:font.familyName]; // If it's not even a prefix then it's probably for a different language if (range.location != 0) diff --git a/src/mkv_wrap.cpp b/src/mkv_wrap.cpp index e0f69972a4..ea9dc3a575 100644 --- a/src/mkv_wrap.cpp +++ b/src/mkv_wrap.cpp @@ -120,8 +120,12 @@ struct MkvStdIO final : InputStream { } }; +static constexpr size_t max_decompressed_subtitle_frame_bytes = 16U * 1024U * 1024U; +static constexpr size_t max_total_subtitle_bytes = 64U * 1024U * 1024U; + static bool read_subtitles(agi::ProgressSink *ps, MatroskaFile *file, MkvStdIO *input, bool srt, double totalTime, AssParser *parser, CompressedStream *cs) { std::vector> subList; + size_t totalSubtitleBytes = 0; // Load blocks uint64_t startTime, endTime, filePos; @@ -139,12 +143,19 @@ static bool read_subtitles(agi::ProgressSink *ps, MatroskaFile *file, MkvStdIO * if (cs) { cs_NextFrame(cs, filePos, frameSize); - int bytesRead = 0; + size_t bytesRead = 0; + + while (true) { + if (bytesRead == uncompBuf.size()) { + if (uncompBuf.size() >= max_decompressed_subtitle_frame_bytes) { + ps->Log("Decompressed subtitle frame exceeds the 16 MiB limit"); + return false; + } + uncompBuf.resize(std::min(uncompBuf.size() * 2, max_decompressed_subtitle_frame_bytes)); + } - int res; - do { - res = cs_ReadData(cs, &uncompBuf[bytesRead], uncompBuf.size() - bytesRead); - if (res == -1) { + int res = cs_ReadData(cs, uncompBuf.data() + bytesRead, static_cast(uncompBuf.size() - bytesRead)); + if (res < 0) { const char *err = cs_GetLastError(cs); if (!err) err = "Unknown error"; ps->Log("Failed to decompress subtitles: " + std::string(err)); @@ -152,16 +163,21 @@ static bool read_subtitles(agi::ProgressSink *ps, MatroskaFile *file, MkvStdIO * } bytesRead += res; + if (res == 0) + break; + } - if (bytesRead >= std::ssize(uncompBuf)) - uncompBuf.resize(2 * std::ssize(uncompBuf)); - } while (res != 0); - - readBuf = std::string_view(&uncompBuf[0], bytesRead); + readBuf = std::string_view(uncompBuf.data(), bytesRead); } else { readBuf = std::string_view(input->file.read(filePos, frameSize), frameSize); } + if (readBuf.size() > max_total_subtitle_bytes - totalSubtitleBytes) { + ps->Log("Matroska subtitle data exceeds the 64 MiB limit"); + return false; + } + totalSubtitleBytes += readBuf.size(); + // Get start and end times int64_t timecodeScaleLow = 1000000; agi::Time subStart = startTime / timecodeScaleLow; diff --git a/src/project.cpp b/src/project.cpp index bdc0904d6f..0a5f24745c 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -44,7 +44,9 @@ #include #include +#include #include +#include #include Project::Project(agi::Context *c) : context(c) { @@ -189,6 +191,69 @@ void Project::LoadUnloadFiles(ProjectProperties properties) { auto timecodes = context->path->MakeAbsolute(properties.timecodes_file, "?script"); auto keyframes = context->path->MakeAbsolute(properties.keyframes_file, "?script"); +#ifdef _WIN32 + auto is_remote_or_device_path = [](agi::fs::path const& path) { + return path.generic_string().starts_with("//"); + }; + auto ordinary_unc_share = [](agi::fs::path const& path) { + auto value = path.generic_string(); + if (!value.starts_with("//") || value.starts_with("//?/") || value.starts_with("//./")) + return std::string(); + + auto server_end = value.find('/', 2); + if (server_end == std::string::npos || server_end == 2) + return std::string(); + auto share_end = value.find('/', server_end + 1); + if (share_end == server_end + 1) + return std::string(); + + auto share = value.substr(0, share_end); + boost::to_lower(share); + return share; + }; + + auto subtitle_share = ordinary_unc_share(context->subsController->Filename()); + auto is_new_remote_or_device_path = [&](agi::fs::path const& linked, agi::fs::path const& current) { + if (linked == current || !is_remote_or_device_path(linked)) + return false; + + auto linked_share = ordinary_unc_share(linked); + return linked_share.empty() || linked_share != subtitle_share; + }; + + std::vector paths_requiring_authorization; + auto add_path_requiring_authorization = [&](agi::fs::path const& linked, agi::fs::path const& current) { + if (is_new_remote_or_device_path(linked, current) && + std::find(paths_requiring_authorization.begin(), paths_requiring_authorization.end(), linked) == paths_requiring_authorization.end()) + paths_requiring_authorization.push_back(linked); + }; + add_path_requiring_authorization(audio, audio_file); + add_path_requiring_authorization(video, video_file); + add_path_requiring_authorization(timecodes, timecodes_file); + add_path_requiring_authorization(keyframes, keyframes_file); + + if (!paths_requiring_authorization.empty()) { + wxString message = _("The subtitle file references Windows network or device paths. Accessing a network path can expose your Windows credentials, and device paths can access special system objects. Only allow these paths if you trust the subtitle's author.\n\nPaths:"); + for (auto const& linked : paths_requiring_authorization) { + message += "\n "; + message += linked.wstring(); + } + message += _("\n\nDo you want to allow access to these paths?"); + + if (wxMessageBox(message, _("Allow linked network paths?"), + wxYES_NO | wxNO_DEFAULT | wxICON_WARNING | wxCENTRE, context->parent) != wxYES) { + auto reject_remote_or_device_path = [&](agi::fs::path& linked, agi::fs::path const& current) { + if (is_new_remote_or_device_path(linked, current)) + linked = current; + }; + reject_remote_or_device_path(audio, audio_file); + reject_remote_or_device_path(video, video_file); + reject_remote_or_device_path(timecodes, timecodes_file); + reject_remote_or_device_path(keyframes, keyframes_file); + } + } +#endif + if (video == video_file && audio == audio_file && keyframes == keyframes_file && timecodes == timecodes_file) return; @@ -232,6 +297,43 @@ void Project::LoadUnloadFiles(ProjectProperties properties) { return; } + auto is_linked_avisynth = [](agi::fs::path const& linked, agi::fs::path const& current) { + return linked != current && agi::fs::HasExtension(linked, "avs"); + }; + bool avisynth_audio = is_linked_avisynth(audio, audio_file); + bool avisynth_video = is_linked_avisynth(video, video_file); + if (avisynth_audio || avisynth_video) { + wxString script_list; + if (avisynth_audio) { + script_list += "\n "; + script_list += audio.wstring(); + } + if (avisynth_video && (!avisynth_audio || video != audio)) { + script_list += "\n "; + script_list += video.wstring(); + } + + bool multiple_scripts = avisynth_audio && avisynth_video && audio != video; + wxString message = multiple_scripts + ? _("The subtitle file references AviSynth scripts. AviSynth scripts can execute arbitrary code with your user permissions. Only load them if you trust the subtitle's author.\n\nScripts:") + : _("The subtitle file references an AviSynth script. AviSynth scripts can execute arbitrary code with your user permissions. Only load it if you trust the subtitle's author.\n\nScript:"); + message += script_list; + message += multiple_scripts + ? _("\n\nDo you want to load these scripts now?") + : _("\n\nDo you want to load this script now?"); + + wxMessageDialog dlg( + context->parent, message, _("Load linked AviSynth script?"), + wxYES_NO | wxNO_DEFAULT | wxICON_WARNING | wxCENTRE); + dlg.SetYesNoLabels(_("Trust author && load"), _("Do not load")); + if (dlg.ShowModal() != wxID_YES) { + if (avisynth_audio) + audio = audio_file; + if (avisynth_video) + video = video_file; + } + } + bool loaded_video = false; if (video != video_file) { if (video.empty()) @@ -241,10 +343,23 @@ void Project::LoadUnloadFiles(ProjectProperties properties) { vc->JumpToFrame(properties.video_position); auto ar_mode = static_cast(properties.ar_mode); - if (ar_mode == AspectRatio::Custom) - vc->SetAspectRatio(properties.ar_value); - else - vc->SetAspectRatio(ar_mode); + switch (ar_mode) { + case AspectRatio::Default: + case AspectRatio::Fullscreen: + case AspectRatio::Widescreen: + case AspectRatio::Cinematic: + vc->SetAspectRatio(ar_mode); + break; + case AspectRatio::Custom: + if (std::isfinite(properties.ar_value)) + vc->SetAspectRatio(properties.ar_value); + else + vc->SetAspectRatio(AspectRatio::Default); + break; + default: + vc->SetAspectRatio(AspectRatio::Default); + break; + } context->videoDisplay->SetWindowZoom(properties.video_zoom); } } diff --git a/src/subtitle_format_srt.cpp b/src/subtitle_format_srt.cpp index fb0ea25c9c..684706b503 100644 --- a/src/subtitle_format_srt.cpp +++ b/src/subtitle_format_srt.cpp @@ -126,12 +126,11 @@ std::string WriteSRTTime(agi::Time const& ts) } SrtTagParser::SrtTagParser() -: tag_matcher("^(.*?)<(/?b|/?i|/?u|/?s|/?font)([^>]*)>(.*)$", boost::regex::icase) -, attrib_matcher(R"(^[[:space:]]+(face|size|color)=('[^']*'|"[^"]*"|[^[:space:]]+))", boost::regex::icase) -, is_quoted(R"(^(['"]).*\1$)") +: tag_matcher("<(/?b|/?i|/?u|/?s|/?font)([^>]*)>", boost::regex::icase) +, attrib_matcher(R"([[:space:]]+(face|size|color)=('[^']*'|"[^"]*"|[^[:space:]]+))", boost::regex::icase) {} -std::string SrtTagParser::ToAss(std::string srt) { +std::string SrtTagParser::ToAss(std::string const& srt) { ToggleTag bold('b'); ToggleTag italic('i'); ToggleTag underline('u'); @@ -139,27 +138,26 @@ std::string SrtTagParser::ToAss(std::string srt) { std::vector font_stack; std::string ass; // result to be built + ass.reserve(srt.size()); - while (!srt.empty()) + auto cursor = srt.cbegin(); + while (cursor != srt.cend()) { - boost::smatch result; - if (!regex_match(srt, result, tag_matcher)) + boost::match_results result; + if (!regex_search(cursor, srt.cend(), result, tag_matcher)) { // no more tags could be matched, end of string - ass.append(srt); + ass.append(cursor, srt.cend()); break; } // we found a tag, translate it - std::string pre_text = result.str(1); - std::string tag_name = result.str(2); - std::string tag_attrs = result.str(3); - std::string post_text = result.str(4); + std::string tag_name = result.str(1); + std::string tag_attrs = result.str(2); // the text before the tag goes through unchanged - ass.append(pre_text); - // the text after the tag is the input for next iteration - srt = post_text; + ass.append(cursor, result[0].first); + cursor = result[0].second; boost::to_lower(tag_name); switch (type_from_name(tag_name)) @@ -182,8 +180,9 @@ std::string SrtTagParser::ToAss(std::string srt) { old_attribs = font_stack.back(); new_attribs = old_attribs; // now find all attributes on this font tag - boost::smatch result; - while (regex_search(tag_attrs, result, attrib_matcher)) + auto attr_cursor = tag_attrs.cbegin(); + boost::match_results result; + while (regex_search(attr_cursor, tag_attrs.cend(), result, attrib_matcher, boost::match_continuous)) { // get attribute name and values std::string attr_name = result.str(1); @@ -191,7 +190,7 @@ std::string SrtTagParser::ToAss(std::string srt) { // clean them boost::to_lower(attr_name); - if (regex_match(attr_value, is_quoted)) + if (attr_value.size() >= 2 && (attr_value.front() == '\'' || attr_value.front() == '"') && attr_value.back() == attr_value.front()) attr_value = attr_value.substr(1, attr_value.size() - 2); // handle the attributes @@ -202,8 +201,7 @@ std::string SrtTagParser::ToAss(std::string srt) { else if (attr_name == "color") new_attribs.color = agi::format("{\\c%s}", agi::Color(attr_value).GetAssOverrideFormatted()); - // remove this attribute to prepare for the next - tag_attrs = result.suffix().str(); + attr_cursor = result[0].second; } // the attributes changed from old are then written out diff --git a/src/subtitle_format_srt.h b/src/subtitle_format_srt.h index ddd24cef3b..532dfaedca 100644 --- a/src/subtitle_format_srt.h +++ b/src/subtitle_format_srt.h @@ -49,12 +49,11 @@ class SrtTagParser { const boost::regex tag_matcher; const boost::regex attrib_matcher; - const boost::regex is_quoted; public: SrtTagParser(); - std::string ToAss(std::string srt); + std::string ToAss(std::string const& srt); }; class SRTSubtitleFormat final : public SubtitleFormat { diff --git a/src/video_display.cpp b/src/video_display.cpp index 6793373600..70210d929b 100644 --- a/src/video_display.cpp +++ b/src/video_display.cpp @@ -54,6 +54,7 @@ #include +#include #include #include #include @@ -527,8 +528,8 @@ void VideoDisplay::OnKeyDown(wxKeyEvent &event) { } void VideoDisplay::SetWindowZoom(double value) { - if (value == 0) return; - windowZoomValue = std::max(value, .125); + if (!std::isfinite(value) || value <= 0) return; + windowZoomValue = std::clamp(value, .125, 10.); size_t selIndex = windowZoomValue / .125 - 1; if (selIndex < zoomBox->GetCount()) zoomBox->SetSelection(selIndex); diff --git a/src/video_provider_dummy.cpp b/src/video_provider_dummy.cpp index 9dff23cf07..99efcf7ebe 100644 --- a/src/video_provider_dummy.cpp +++ b/src/video_provider_dummy.cpp @@ -45,13 +45,36 @@ #include #include +#include + +namespace { +constexpr size_t max_dummy_frame_bytes = 256U * 1024U * 1024U; + +size_t checked_frame_size(int width, int height) { + if (width <= 0 || height <= 0) + throw VideoOpenError("Dummy video resolution must be positive"); + if (width > std::numeric_limits::max() / 4) + throw VideoOpenError("Dummy video width is too large"); + + auto w = static_cast(width); + auto h = static_cast(height); + if (h > max_dummy_frame_bytes / 4 / w) + throw VideoOpenError("Dummy video frame is too large"); + + return w * h * 4; +} +} + DummyVideoProvider::DummyVideoProvider(agi::vfr::Framerate fps, int frames, int width, int height, agi::Color colour, bool pattern) : framecount(frames) , fps(fps) , width(width) , height(height) { - data.resize(width * height * 4); + if (frames <= 0) + throw VideoOpenError("Dummy video frame count must be positive"); + + data.resize(checked_frame_size(width, height)); auto red = colour.r; auto green = colour.g; diff --git a/src/video_provider_yuv4mpeg.cpp b/src/video_provider_yuv4mpeg.cpp index ef8e0ea66c..fd4f932767 100644 --- a/src/video_provider_yuv4mpeg.cpp +++ b/src/video_provider_yuv4mpeg.cpp @@ -43,6 +43,7 @@ #include #include +#include #include #include @@ -51,6 +52,8 @@ namespace { +constexpr size_t max_decoded_frame_bytes = 256U * 1024U * 1024U; + /// @class YUV4MPEGVideoProvider /// @brief Implements reading of YUV4MPEG uncompressed video files class YUV4MPEGVideoProvider final : public VideoProvider { @@ -114,9 +117,10 @@ class YUV4MPEGVideoProvider final : public VideoProvider { int w = 0, h = 0; /// frame width/height int num_frames = -1; /// length of file in frames - int frame_sz; /// size of each frame in bytes - int luma_sz; /// size of the luma plane of each frame, in bytes - int chroma_sz; /// size of one of the two chroma planes of each frame, in bytes + size_t frame_sz = 0; /// size of each frame in bytes + size_t luma_sz = 0; /// size of the luma plane of each frame, in bytes + size_t chroma_sz = 0; /// size of one of the two chroma planes of each frame, in bytes + size_t decoded_frame_sz = 0; /// size of a decoded BGRA frame in bytes Y4M_PixelFormat pixfmt = Y4M_PIXFMT_NONE; /// colorspace/pixel format Y4M_InterlacingMode imode = Y4M_ILACE_NOTSET; /// interlacing mode (for the entire stream) @@ -170,27 +174,38 @@ YUV4MPEGVideoProvider::YUV4MPEGVideoProvider(agi::fs::path const& filename) if (w <= 0 || h <= 0) throw VideoOpenError("Invalid resolution"); + if ((w & 1) || (h & 1)) + throw VideoOpenError("YUV4MPEG 4:2:0 resolution must be even"); + if (w > std::numeric_limits::max() / 4) + throw VideoOpenError("YUV4MPEG width is too large"); if (fps_rat.num <= 0 || fps_rat.den <= 0) { fps_rat.num = 25; fps_rat.den = 1; LOG_D("provider/video/yuv4mpeg") << "framerate info unavailable, assuming 25fps"; } + fps = double(fps_rat.num) / fps_rat.den; if (pixfmt == Y4M_PIXFMT_NONE) pixfmt = Y4M_PIXFMT_420JPEG; if (imode == Y4M_ILACE_NOTSET) imode = Y4M_ILACE_UNKNOWN; - luma_sz = w * h; + auto width = static_cast(w); + auto height = static_cast(h); + if (height > max_decoded_frame_bytes / 4 / width) + throw VideoOpenError("YUV4MPEG frame is too large"); + + luma_sz = width * height; + decoded_frame_sz = luma_sz * 4; switch (pixfmt) { case Y4M_PIXFMT_420JPEG: case Y4M_PIXFMT_420MPEG2: case Y4M_PIXFMT_420PALDV: - chroma_sz = (w * h) >> 2; break; + chroma_sz = luma_sz / 4; break; default: /// @todo add support for more pixel formats throw VideoOpenError("Unsupported pixel format"); } - frame_sz = luma_sz + chroma_sz*2; + frame_sz = luma_sz + chroma_sz * 2; num_frames = IndexFile(pos); if (num_frames <= 0 || seek_table.empty()) @@ -332,7 +347,6 @@ void YUV4MPEGVideoProvider::ParseFileHeader(const std::vector& tags fps_rat.den = t_fps_den; pixfmt = t_pixfmt != Y4M_PIXFMT_NONE ? t_pixfmt : Y4M_PIXFMT_420JPEG; imode = t_imode != Y4M_ILACE_NOTSET ? t_imode : Y4M_ILACE_UNKNOWN; - fps = double(fps_rat.num) / fps_rat.den; inited = true; } } @@ -372,8 +386,14 @@ int YUV4MPEGVideoProvider::IndexFile(uint64_t pos) { } else if (tags.front() == "FRAME") flags = ParseFrameHeader(tags); + else + throw VideoOpenError("IndexFile: malformed frame header"); if (flags == Y4M_FFLAG_NONE) { + if (pos > file.size() || frame_sz > file.size() - pos) + throw VideoOpenError("IndexFile: truncated frame data"); + if (framecount == std::numeric_limits::max()) + throw VideoOpenError("IndexFile: too many frames"); framecount++; seek_table.push_back(pos); pos += frame_sz; @@ -391,11 +411,11 @@ void YUV4MPEGVideoProvider::GetFrame(int n, VideoFrame &frame) { int uv_width = w / 2; - auto src_y = reinterpret_cast(file.read(seek_table[n], luma_sz + chroma_sz * 2)); + auto src_y = reinterpret_cast(file.read(seek_table[n], frame_sz)); auto src_u = src_y + luma_sz; auto src_v = src_u + chroma_sz; - frame.data.resize(w * h * 4); - unsigned char *dst = &frame.data[0]; + frame.data.resize(decoded_frame_sz); + unsigned char *dst = frame.data.data(); for (int py = 0; py < h; ++py) { for (int px = 0; px < w / 2; ++px) { diff --git a/tests/lua/sibling/moonscript.lua b/tests/lua/sibling/moonscript.lua new file mode 100644 index 0000000000..bebafe2d77 --- /dev/null +++ b/tests/lua/sibling/moonscript.lua @@ -0,0 +1,2 @@ +sibling_moonscript_loaded = true +return { loadstring = loadstring } diff --git a/tests/tests/fs.cpp b/tests/tests/fs.cpp index 0f066860f3..e191ca8f13 100644 --- a/tests/tests/fs.cpp +++ b/tests/tests/fs.cpp @@ -15,9 +15,12 @@ // Aegisub Project http://www.aegisub.org/ #include +#include +#include #include #include +#include #include using namespace agi::fs; @@ -64,6 +67,15 @@ TEST(lagi_fs, file_size) { EXPECT_THROW(Size("data/dir"), NotAFile); } +TEST(lagi_fs, file_mapping_rejects_invalid_ranges) { + agi::read_file_mapping file("data/ten_bytes"); + EXPECT_THROW(file.read(-1, 0), agi::InternalError); + EXPECT_THROW(file.read(-1, 1), agi::InternalError); + EXPECT_THROW(file.read(9, 2), agi::InternalError); + EXPECT_THROW(file.read(1, std::numeric_limits::max()), agi::InternalError); + EXPECT_NO_THROW(file.read(10, 0)); +} + TEST(lagi_fs, touch_creates_file) { Remove("data/touch_tmp"); ASSERT_FALSE(Exists("data/touch_tmp")); diff --git a/tests/tests/line_iterator.cpp b/tests/tests/line_iterator.cpp index 41e6ae2f92..c4ca22b3d9 100644 --- a/tests/tests/line_iterator.cpp +++ b/tests/tests/line_iterator.cpp @@ -61,6 +61,19 @@ TEST(lagi_line, int) { expect_eq("1.0\n2.0\n3.0\n4.0", 1, 2, 3, 4); expect_eq(" 0x16 \n 09 \n -2", 0, 9, -2); } +TEST(lagi_line, many_invalid_ints) { + std::string input; + for (int i = 0; i < 100000; ++i) + input += "invalid\n"; + input += "42"; + + std::stringstream stream(input); + agi::line_iterator iter(stream); + ASSERT_FALSE(iter == end(iter)); + EXPECT_EQ(42, *iter); + EXPECT_NO_THROW(++iter); + EXPECT_EQ(iter, end(iter)); +} TEST(lagi_line, double) { expect_eq("1.0\n2.0", 1.0, 2.0); expect_eq("#1.0\n\t2.5", 2.5); diff --git a/tests/tests/lua_lfs.cpp b/tests/tests/lua_lfs.cpp index 29c421869c..0bfd898a22 100644 --- a/tests/tests/lua_lfs.cpp +++ b/tests/tests/lua_lfs.cpp @@ -18,11 +18,14 @@ #include #include +#include #include #include #include +#include + namespace { // The lfs entry points are static in lua/modules/lfs.cpp and are only reachable // through the FFI table `luaopen_lfs_impl()` builds, so call `get_mode()` the way @@ -102,3 +105,14 @@ TEST_F(lagi_lua_lfs, get_mode_handles_non_ascii_paths) { EXPECT_EQ("directory", get_mode(nonascii_dir)); EXPECT_EQ("file", get_mode(nonascii_file)); } + +TEST_F(lagi_lua_lfs, mandatory_support_is_not_loaded_from_script_directory) { + auto sibling = util::test_data_dir() / "lua/sibling"; + auto support = util::test_data_dir().parent_path() / "automation/include"; + + ASSERT_TRUE(agi::lua::Install(L, {sibling}, support)) << lua_tostring(L, -1); + + lua_getglobal(L, "sibling_moonscript_loaded"); + EXPECT_TRUE(lua_isnil(L, -1)); + lua_pop(L, 1); +} diff --git a/tests/tests/vfr.cpp b/tests/tests/vfr.cpp index 752a33aafd..0eba435f7f 100644 --- a/tests/tests/vfr.cpp +++ b/tests/tests/vfr.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -47,6 +48,9 @@ TEST(lagi_vfr, constructors_good) { TEST(lagi_vfr, constructors_bad_cfr) { EXPECT_THROW(Framerate(-1.), InvalidFramerate); EXPECT_THROW(Framerate(1000.1), InvalidFramerate); + EXPECT_THROW(std::ignore = Framerate(std::numeric_limits::quiet_NaN()), InvalidFramerate); + EXPECT_THROW(std::ignore = Framerate(std::numeric_limits::infinity()), InvalidFramerate); + EXPECT_THROW(std::ignore = Framerate(std::numeric_limits::max()), InvalidFramerate); } TEST(lagi_vfr, constructors_bad_timecodes) { @@ -70,6 +74,9 @@ TEST(lagi_vfr, constructors_bad_v1) { EXPECT_THROW(Framerate(input_dir / "v1_override_zero.txt"), InvalidFramerate); EXPECT_THROW(Framerate(input_dir / "v1_negative_start_of_range.txt"), InvalidFramerate); EXPECT_THROW(Framerate(input_dir / "v1_end_less_than_start.txt"), InvalidFramerate); + EXPECT_THROW(Framerate(input_dir / "v1_range_too_large.txt"), InvalidFramerate); + EXPECT_THROW(Framerate(input_dir / "v1_nan_fps.txt"), InvalidFramerate); + EXPECT_THROW(Framerate(input_dir / "v1_tiny_fps.txt"), InvalidFramerate); } TEST(lagi_vfr, constructors_bad_v2) { diff --git a/tests/vfr/v1_nan_fps.txt b/tests/vfr/v1_nan_fps.txt new file mode 100644 index 0000000000..1d47b16d3f --- /dev/null +++ b/tests/vfr/v1_nan_fps.txt @@ -0,0 +1,3 @@ +# timecode format v1 +Assume nan +0,1,24 diff --git a/tests/vfr/v1_range_too_large.txt b/tests/vfr/v1_range_too_large.txt new file mode 100644 index 0000000000..dec1ab7b53 --- /dev/null +++ b/tests/vfr/v1_range_too_large.txt @@ -0,0 +1,3 @@ +# timecode format v1 +Assume 24 +0,2147483647,24 diff --git a/tests/vfr/v1_tiny_fps.txt b/tests/vfr/v1_tiny_fps.txt new file mode 100644 index 0000000000..cbe16a7e92 --- /dev/null +++ b/tests/vfr/v1_tiny_fps.txt @@ -0,0 +1,3 @@ +# timecode format v1 +Assume 24 +0,1,1e-300