diff --git a/automation/tests/aegisub.cpp b/automation/tests/aegisub.cpp index f89d83249d..1b4cba89cd 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 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..c440cbf5c2 100644 --- a/libaegisub/common/vfr.cpp +++ b/libaegisub/common/vfr.cpp @@ -19,8 +19,10 @@ #include "libaegisub/vfr.h" #include "libaegisub/charset.h" +#include "libaegisub/format.h" #include "libaegisub/io.h" #include "libaegisub/line_iterator.h" +#include "libaegisub/util.h" #include #include @@ -28,12 +30,28 @@ #include #include #include +#include namespace { static const int64_t default_denominator = 1000000000; +static const int 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 +94,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 > max_timecodes - 2) + throw InvalidFramerate(agi::format("V1 timecode range exceeds the %d frame limit", max_timecodes)); + 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 +112,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 +142,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 +162,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 +205,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 (std::ssize(timecodes) >= 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..6e5ec17002 100644 --- a/src/ass_attachment.cpp +++ b/src/ass_attachment.cpp @@ -26,11 +26,16 @@ // Out-of-line to anchor vtable AssEntryGroup AssAttachment::Group() const { return group; } -AssAttachment::AssAttachment(std::string const& header, AssEntryGroup group) -: entry_data(header + "\r\n") -, filename(header.substr(10)) +AssAttachment::AssAttachment(std::string const& data, AssEntryGroup group) +: entry_data(data) , group(group) { + std::string filename_str = data.substr(10); + const auto pos = filename_str.find("\r\n"); + if (pos != std::string::npos) + filename_str.resize(pos); + + filename = filename_str; } AssAttachment::AssAttachment(agi::fs::path const& name, AssEntryGroup group) @@ -49,14 +54,16 @@ AssAttachment::AssAttachment(agi::fs::path const& name, AssEntryGroup group) } 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(filename, true).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..f59e60c79f 100644 --- a/src/ass_attachment.h +++ b/src/ass_attachment.h @@ -33,9 +33,6 @@ 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"; } - /// Extract the contents of this attachment to a file /// @param filename Path to save the attachment to void Extract(agi::fs::path const& filename) const; @@ -47,6 +44,6 @@ class AssAttachment final : public AssEntry { std::string const& GetEntryData() const { return entry_data; } AssEntryGroup Group() const override; - AssAttachment(std::string const& header, AssEntryGroup group); + AssAttachment(std::string const& data, AssEntryGroup group); AssAttachment(agi::fs::path const& name, AssEntryGroup group); }; diff --git a/src/ass_override.cpp b/src/ass_override.cpp index 7e1fe59261..d2e4036d18 100644 --- a/src/ass_override.cpp +++ b/src/ass_override.cpp @@ -362,7 +362,10 @@ 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 + 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..fda5e38fe4 100644 --- a/src/ass_parser.cpp +++ b/src/ass_parser.cpp @@ -103,6 +103,11 @@ AssParser::AssParser(AssFile *target, int version) AssParser::~AssParser() = default; +void AssParser::FinishAttachment() { + target->Attachments.emplace_back(std::move(curr_attachment.data), curr_attachment.group); + curr_attachment.active = false; +} + void AssParser::ParseAttachmentLine(std::string const& data) { bool is_filename = data.starts_with("fontname: ") || data.starts_with("filename: "); @@ -116,15 +121,15 @@ 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); + curr_attachment.data.append(data).append("\r\n"); // Done building if (data.size() < 80) - target->Attachments.push_back(*attach.release()); + FinishAttachment(); } } @@ -187,13 +192,23 @@ void AssParser::ParseStyleLine(std::string const& data) { } void AssParser::ParseFontLine(std::string const& data) { - if (data.starts_with("fontname: ")) - attach = std::make_unique(data, AssEntryGroup::FONT); + if (data.starts_with("fontname: ")) { + curr_attachment = { + .active = true, + .group = AssEntryGroup::FONT, + .data = data + "\r\n", + }; + } } void AssParser::ParseGraphicsLine(std::string const& data) { - if (data.starts_with("filename: ")) - attach = std::make_unique(data, AssEntryGroup::GRAPHIC); + if (data.starts_with("filename: ")) { + curr_attachment = { + .active = true, + .group = AssEntryGroup::GRAPHIC, + .data = data + "\r\n", + }; + } } void AssParser::ParseExtradataLine(std::string const &rawdata) { @@ -235,7 +250,7 @@ void AssParser::AddLine(std::string const& data) { // Special-case for attachments since a line could theoretically be both a // valid attachment data line and a valid section header, and if an // attachment is in progress it needs to be treated as that - if (attach.get()) { + if (curr_attachment.active) { ParseAttachmentLine(data); return; } diff --git a/src/ass_parser.h b/src/ass_parser.h index 243701721a..456a462d2a 100644 --- a/src/ass_parser.h +++ b/src/ass_parser.h @@ -15,7 +15,8 @@ #include #include -class AssAttachment; +#include "ass_entry.h" + class AssFile; class AssParser { @@ -24,9 +25,16 @@ class AssParser { AssFile *target; int version; - std::unique_ptr attach; + + struct { + bool active = false; + AssEntryGroup group; + std::string data; + } curr_attachment; + 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_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..ba56834951 100644 --- a/src/mkv_wrap.cpp +++ b/src/mkv_wrap.cpp @@ -120,8 +120,12 @@ struct MkvStdIO final : InputStream { } }; +static constexpr ptrdiff_t max_decompressed_subtitle_frame_bytes = 16 * 1024 * 1024; +static constexpr ptrdiff_t max_total_subtitle_bytes = 64 * 1024 * 1024; + static bool read_subtitles(agi::ProgressSink *ps, MatroskaFile *file, MkvStdIO *input, bool srt, double totalTime, AssParser *parser, CompressedStream *cs) { std::vector> subList; + ptrdiff_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; + ptrdiff_t bytesRead = 0; + + while (true) { + if (bytesRead >= std::ssize(uncompBuf)) { + if (uncompBuf.size() >= max_decompressed_subtitle_frame_bytes) { + ps->Log(agi::format("Decompressed subtitle frame exceeds the %d MiB limit", max_decompressed_subtitle_frame_bytes / 1024 / 1024)); + return false; + } + uncompBuf.resize(std::min(std::ssize(uncompBuf) * 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(std::ssize(uncompBuf) - 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 (std::ssize(readBuf) > max_total_subtitle_bytes - totalSubtitleBytes) { + ps->Log(agi::format("Matroska subtitle data exceeds the %d MiB limit", max_total_subtitle_bytes / 1024 / 1024)); + return false; + } + totalSubtitleBytes += std::ssize(readBuf); + // 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..28d92fb96a 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -45,6 +45,7 @@ #include #include +#include #include Project::Project(agi::Context *c) : context(c) { @@ -232,6 +233,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. You do not need to load these scripts to view or edit this subtitle file.\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. You do not need to load this script to view or edit this subtitle file.\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 +279,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..f75f2d7f9d 100644 --- a/src/video_provider_dummy.cpp +++ b/src/video_provider_dummy.cpp @@ -45,13 +45,34 @@ #include #include +#include + +namespace { +constexpr ptrdiff_t max_dummy_frame_bytes = 256 * 1024 * 1024; + +ptrdiff_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"); + + if (height > max_dummy_frame_bytes / 4 / width) + throw VideoOpenError("Dummy video frame is too large"); + + return width * height * 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..39c0bc1b36 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 ptrdiff_t max_decoded_frame_bytes = 256 * 1024 * 1024; + /// @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 + ptrdiff_t frame_sz = 0; /// size of each frame in bytes + ptrdiff_t luma_sz = 0; /// size of the luma plane of each frame, in bytes + ptrdiff_t chroma_sz = 0; /// size of one of the two chroma planes of each frame, in bytes + ptrdiff_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) @@ -135,8 +139,8 @@ class YUV4MPEGVideoProvider final : public VideoProvider { void ParseFileHeader(const std::vector& tags); Y4M_FrameFlags ParseFrameHeader(const std::vector& tags); - std::vector ReadHeader(uint64_t &startpos); - int IndexFile(uint64_t pos); + std::vector ReadHeader(int64_t &startpos); + int IndexFile(int64_t pos); public: YUV4MPEGVideoProvider(agi::fs::path const& filename); @@ -165,32 +169,41 @@ YUV4MPEGVideoProvider::YUV4MPEGVideoProvider(agi::fs::path const& filename) if (strncmp("YUV4MPEG2 ", file.read(0, 10), 10)) throw VideoNotSupported("File is not a YUV4MPEG file (bad magic)"); - uint64_t pos = 0; + int64_t pos = 0; ParseFileHeader(ReadHeader(pos)); 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; + if (h > max_decoded_frame_bytes / 4 / w) + throw VideoOpenError("YUV4MPEG frame is too large"); + + luma_sz = static_cast(w) * static_cast(h); + 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()) @@ -200,9 +213,9 @@ YUV4MPEGVideoProvider::YUV4MPEGVideoProvider(agi::fs::path const& filename) /// @brief Read a frame or file header at a given file position /// @param startpos The byte offset at where to start reading /// @return A list of parameters -std::vector YUV4MPEGVideoProvider::ReadHeader(uint64_t &pos) { +std::vector YUV4MPEGVideoProvider::ReadHeader(int64_t &pos) { std::vector tags; - if (pos >= file.size()) + if (pos >= std::ssize(file)) return tags; auto len = std::min(YUV4MPEG_HEADER_MAXLEN, file.size() - pos); @@ -332,7 +345,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; } } @@ -354,7 +366,7 @@ YUV4MPEGVideoProvider::Y4M_FrameFlags YUV4MPEGVideoProvider::ParseFrameHeader(co /// This function goes through the file, finds and parses all file and frame headers, /// and creates a seek table that lists the byte positions of all frames so seeking /// can easily be done. -int YUV4MPEGVideoProvider::IndexFile(uint64_t pos) { +int YUV4MPEGVideoProvider::IndexFile(int64_t pos) { int framecount = 0; // the ParseFileHeader() call in LoadVideo() will already have read @@ -372,8 +384,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 > std::ssize(file) || frame_sz > std::ssize(file) - 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 +409,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