From 5544fcae3d9bba0e327faf7d5c222c9a1a6b9db4 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 27 Aug 2019 17:07:20 +0200 Subject: [PATCH 1/4] throw exception if a file cannot be opened * Add a new function in utils.hpp: open_file_or_throw: This function returns an opened std::ifstream or throws by calling `inja_throw`. * Use this function in Parser::load_file which previously returned an empty string if the file couldn't be opened. * Use this function in Environment::load_json which previously threw a `nlohmann::detail::parse_error` if the file couldn't be opened. * In Parser::parse_statement: When including files through `include`, do not attempt to (re-)parse templates from files that were already included. Additionally, this prevents inja from attempting to load in-memory templates by their name from disk. * Add tests that check if an exception is thrown when attempting to open files that do not exist. --- include/inja/environment.hpp | 3 ++- include/inja/parser.hpp | 8 +++++--- include/inja/utils.hpp | 16 ++++++++++++++++ single_include/inja/inja.hpp | 28 ++++++++++++++++++++++++---- test/unit-files.cpp | 6 ++++++ test/unit-renderer.cpp | 1 + 6 files changed, 54 insertions(+), 8 deletions(-) diff --git a/include/inja/environment.hpp b/include/inja/environment.hpp index 1c7901a3..a590c5f2 100644 --- a/include/inja/environment.hpp +++ b/include/inja/environment.hpp @@ -15,6 +15,7 @@ #include "renderer.hpp" #include "string_view.hpp" #include "template.hpp" +#include "utils.hpp" namespace inja { @@ -144,7 +145,7 @@ class Environment { } json load_json(const std::string& filename) { - std::ifstream file(m_impl->input_path + filename); + std::ifstream file = open_file_or_throw(m_impl->input_path + filename); json j; file >> j; return j; diff --git a/include/inja/parser.hpp b/include/inja/parser.hpp index c2364ba4..9992ea4a 100644 --- a/include/inja/parser.hpp +++ b/include/inja/parser.hpp @@ -384,8 +384,10 @@ class Parser { } // sys::path::remove_dots(pathname, true, sys::path::Style::posix); - Template include_template = parse_template(pathname); - m_included_templates.emplace(pathname, include_template); + if (m_included_templates.find(pathname) == m_included_templates.end()) { + Template include_template = parse_template(pathname); + m_included_templates.emplace(pathname, include_template); + } // generate a reference bytecode tmpl.bytecodes.emplace_back(Bytecode::Op::Include, json(pathname), Bytecode::Flag::ValueImmediate); @@ -505,7 +507,7 @@ class Parser { } std::string load_file(nonstd::string_view filename) { - std::ifstream file(static_cast(filename)); + std::ifstream file = open_file_or_throw(static_cast(filename)); std::string text((std::istreambuf_iterator(file)), std::istreambuf_iterator()); return text; } diff --git a/include/inja/utils.hpp b/include/inja/utils.hpp index e791af52..124f54f6 100644 --- a/include/inja/utils.hpp +++ b/include/inja/utils.hpp @@ -1,6 +1,7 @@ #ifndef PANTOR_INJA_UTILS_HPP #define PANTOR_INJA_UTILS_HPP +#include #include #include "string_view.hpp" @@ -12,6 +13,21 @@ inline void inja_throw(const std::string& type, const std::string& message) { throw std::runtime_error("[inja.exception." + type + "] " + message); } +inline std::ifstream open_file_or_throw(const std::string& path) +{ + std::ifstream file; + file.exceptions(std::ifstream::failbit | std::ifstream::badbit); + try + { + file.open(path); + } + catch( const std::ios_base::failure& e ) + { + inja_throw("file_error", "failed accessing file at '" + path + "'"); + } + return file; +} + namespace string_view { inline nonstd::string_view slice(nonstd::string_view view, size_t start, size_t end) { start = std::min(start, view.size()); diff --git a/single_include/inja/inja.hpp b/single_include/inja/inja.hpp index 679b4203..3fc0d5e2 100644 --- a/single_include/inja/inja.hpp +++ b/single_include/inja/inja.hpp @@ -1693,6 +1693,7 @@ struct Token { #ifndef PANTOR_INJA_UTILS_HPP #define PANTOR_INJA_UTILS_HPP +#include #include // #include "string_view.hpp" @@ -1705,6 +1706,21 @@ inline void inja_throw(const std::string& type, const std::string& message) { throw std::runtime_error("[inja.exception." + type + "] " + message); } +inline std::ifstream open_file_or_throw(const std::string& path) +{ + std::ifstream file; + file.exceptions(std::ifstream::failbit | std::ifstream::badbit); + try + { + file.open(path); + } + catch( const std::ios_base::failure& e ) + { + inja_throw("file_error", "failed accessing file at '" + path + "'"); + } + return file; +} + namespace string_view { inline nonstd::string_view slice(nonstd::string_view view, size_t start, size_t end) { start = std::min(start, view.size()); @@ -2384,8 +2400,10 @@ class Parser { } // sys::path::remove_dots(pathname, true, sys::path::Style::posix); - Template include_template = parse_template(pathname); - m_included_templates.emplace(pathname, include_template); + if (m_included_templates.find(pathname) == m_included_templates.end()) { + Template include_template = parse_template(pathname); + m_included_templates.emplace(pathname, include_template); + } // generate a reference bytecode tmpl.bytecodes.emplace_back(Bytecode::Op::Include, json(pathname), Bytecode::Flag::ValueImmediate); @@ -2505,7 +2523,7 @@ class Parser { } std::string load_file(nonstd::string_view filename) { - std::ifstream file(static_cast(filename)); + std::ifstream file = open_file_or_throw(static_cast(filename)); std::string text((std::istreambuf_iterator(file)), std::istreambuf_iterator()); return text; } @@ -3184,6 +3202,8 @@ class Renderer { // #include "template.hpp" +// #include "utils.hpp" + namespace inja { @@ -3313,7 +3333,7 @@ class Environment { } json load_json(const std::string& filename) { - std::ifstream file(m_impl->input_path + filename); + std::ifstream file = open_file_or_throw(m_impl->input_path + filename); json j; file >> j; return j; diff --git a/test/unit-files.cpp b/test/unit-files.cpp index 25d3ca4e..afd7614f 100644 --- a/test/unit-files.cpp +++ b/test/unit-files.cpp @@ -23,6 +23,12 @@ TEST_CASE("loading") { SECTION("File includes should be rendered") { CHECK( env.render_file(test_file_directory + "include.txt", data) == "Answer: Hello Jeff." ); } + + SECTION("File error should throw") { + std::string path(test_file_directory + "does-not-exist"); + CHECK_THROWS_WITH( env.load_file(path), "[inja.exception.file_error] failed accessing file at '" + path + "'" ); + CHECK_THROWS_WITH( env.load_json(path), "[inja.exception.file_error] failed accessing file at '" + path + "'" ); + } } TEST_CASE("complete-files") { diff --git a/test/unit-renderer.cpp b/test/unit-renderer.cpp index 6380948c..310eb411 100644 --- a/test/unit-renderer.cpp +++ b/test/unit-renderer.cpp @@ -378,6 +378,7 @@ TEST_CASE("templates") { inja::Template t2 = env.parse("{% include \"greeting\" %}!"); CHECK( env.render(t2, data) == "Hello Peter!" ); + CHECK_THROWS_WITH( env.parse("{% include \"does-not-exist\" %}!"), "[inja.exception.file_error] failed accessing file at 'does-not-exist'" ); } } From e1ea66000768f2be8ddccd969634be46181e163a Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 30 Aug 2019 13:31:00 +0200 Subject: [PATCH 2/4] cmake: enable C++11 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c7b0e990..d9fba964 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,7 +36,7 @@ target_include_directories(inja INTERFACE $ ) -# target_compile_features(inja INTERFACE cxx_std_11) +target_compile_features(inja INTERFACE cxx_std_11) set(INJA_PACKAGE_USE_EMBEDDED_JSON OFF) From 111f6feac54cc8b651dccdaf2b1ecd66d5382d5c Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 30 Aug 2019 13:53:31 +0200 Subject: [PATCH 3/4] cmake: require C++11 when depending on single_inja --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index d9fba964..4c4f719b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,7 @@ if(BUILD_TESTING) add_library(single_inja INTERFACE) + target_compile_features(single_inja INTERFACE cxx_std_11) target_include_directories(single_inja INTERFACE single_include include third_party/include) add_executable(single_inja_test From be699e5dca62c250d4ca810b0a8434a2c619db3b Mon Sep 17 00:00:00 2001 From: pantor Date: Sun, 8 Sep 2019 15:00:26 +0200 Subject: [PATCH 4/4] code style --- include/inja/utils.hpp | 10 +++------- single_include/inja/inja.hpp | 10 +++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/include/inja/utils.hpp b/include/inja/utils.hpp index 124f54f6..77c65c86 100644 --- a/include/inja/utils.hpp +++ b/include/inja/utils.hpp @@ -13,16 +13,12 @@ inline void inja_throw(const std::string& type, const std::string& message) { throw std::runtime_error("[inja.exception." + type + "] " + message); } -inline std::ifstream open_file_or_throw(const std::string& path) -{ +inline std::ifstream open_file_or_throw(const std::string& path) { std::ifstream file; file.exceptions(std::ifstream::failbit | std::ifstream::badbit); - try - { + try { file.open(path); - } - catch( const std::ios_base::failure& e ) - { + } catch(const std::ios_base::failure& e) { inja_throw("file_error", "failed accessing file at '" + path + "'"); } return file; diff --git a/single_include/inja/inja.hpp b/single_include/inja/inja.hpp index 3fc0d5e2..0cb64f2d 100644 --- a/single_include/inja/inja.hpp +++ b/single_include/inja/inja.hpp @@ -1706,16 +1706,12 @@ inline void inja_throw(const std::string& type, const std::string& message) { throw std::runtime_error("[inja.exception." + type + "] " + message); } -inline std::ifstream open_file_or_throw(const std::string& path) -{ +inline std::ifstream open_file_or_throw(const std::string& path) { std::ifstream file; file.exceptions(std::ifstream::failbit | std::ifstream::badbit); - try - { + try { file.open(path); - } - catch( const std::ios_base::failure& e ) - { + } catch(const std::ios_base::failure& e) { inja_throw("file_error", "failed accessing file at '" + path + "'"); } return file;