From 29c30e973f8159f1c68acd38a7fbda1d99046809 Mon Sep 17 00:00:00 2001 From: alandefreitas Date: Thu, 13 Oct 2022 17:55:28 -0300 Subject: [PATCH] finicky example fix #165 --- CMakeLists.txt | 2 +- doc/qbk/0.main.qbk | 2 + doc/qbk/6.0.examples.qbk | 4 + example/CMakeLists.txt | 1 + example/finicky/CMakeLists.txt | 19 ++ example/finicky/Jamfile | 17 ++ example/finicky/config.json | 38 ++++ example/finicky/finicky.cpp | 394 +++++++++++++++++++++++++++++++++ 8 files changed, 476 insertions(+), 1 deletion(-) create mode 100644 example/finicky/CMakeLists.txt create mode 100644 example/finicky/Jamfile create mode 100644 example/finicky/config.json create mode 100644 example/finicky/finicky.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bf24225c..f3cbeaef 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,7 +50,7 @@ endif() if (BOOST_URL_FIND_PACKAGE_BOOST) find_package(Boost 1.78.0 REQUIRED COMPONENTS container) elseif (BOOST_URL_IS_ROOT) - set(BOOST_URL_UNIT_TEST_LIBRARIES container filesystem unordered) + set(BOOST_URL_UNIT_TEST_LIBRARIES container filesystem unordered json regex) set(BOOST_INCLUDE_LIBRARIES url json ${BOOST_URL_UNIT_TEST_LIBRARIES}) set(BOOST_EXCLUDE_LIBRARIES url) set(CMAKE_FOLDER Dependencies) diff --git a/doc/qbk/0.main.qbk b/doc/qbk/0.main.qbk index 1427b0e1..51be8eec 100644 --- a/doc/qbk/0.main.qbk +++ b/doc/qbk/0.main.qbk @@ -130,6 +130,8 @@ [/-----------------------------------------------------------------------------] +[import ../../example/qrcode/qrcode.cpp] +[import ../../example/finicky/finicky.cpp] [import ../../example/mailto/mailto.cpp] [import ../../example/magnet/magnet.cpp] [import ../../example/route/route.cpp] diff --git a/doc/qbk/6.0.examples.qbk b/doc/qbk/6.0.examples.qbk index a4c9ecfd..82e898d7 100644 --- a/doc/qbk/6.0.examples.qbk +++ b/doc/qbk/6.0.examples.qbk @@ -16,6 +16,10 @@ [example_qrcode] [endsect] +[section Finicky] +[example_finicky] +[endsect] + [section mailto URLs] [example_mailto] [endsect] diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 78df9f96..0ca4b009 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -8,6 +8,7 @@ # add_subdirectory(qrcode) +add_subdirectory(finicky) add_subdirectory(mailto) add_subdirectory(magnet) add_subdirectory(route) diff --git a/example/finicky/CMakeLists.txt b/example/finicky/CMakeLists.txt new file mode 100644 index 00000000..33f98811 --- /dev/null +++ b/example/finicky/CMakeLists.txt @@ -0,0 +1,19 @@ +# +# Copyright (c) 2022 Alan de Freitas (alandefreitas@gmail.com) +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +# Official repository: https://github.com/boostorg/url +# + +source_group("" FILES + finicky.cpp + ) + +add_executable(finicky + finicky.cpp + ) + +set_property(TARGET finicky PROPERTY FOLDER "Examples") +target_link_libraries(finicky PRIVATE Boost::url Boost::json Boost::regex) diff --git a/example/finicky/Jamfile b/example/finicky/Jamfile new file mode 100644 index 00000000..6a1a5969 --- /dev/null +++ b/example/finicky/Jamfile @@ -0,0 +1,17 @@ +# +# Copyright (c) 2022 Alan de Freitas (alandefreitas@gmail.com) +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +# Official repository: https://github.com/boostorg/url +# + +project : requirements ; + +project + : requirements + /boost/url//boost_url + ; + +exe qrcode : qrcode.cpp ; diff --git a/example/finicky/config.json b/example/finicky/config.json new file mode 100644 index 00000000..201b5d48 --- /dev/null +++ b/example/finicky/config.json @@ -0,0 +1,38 @@ +{ + "defaultBrowser": "Google Chrome", + "rewrite": [ + { + "match": { + "protocol": "http" + }, + "url": { + "protocol": "https" + } + }, + { + "match": "**/*example.org/*", + "url": "http://example.com" + } + ], + "handlers": [ + { + "match": [ + "**/*apple.com/*", + "**/*example.org/*", + "**/*example.com**" + ], + "browser": "Safari" + }, + { + "match": "/workplace/", + "browser": "Firefox" + }, + { + "match": [ + "**/*google.com/*", + "**/*.google.com/*" + ], + "browser": "Google Chrome" + } + ] +} \ No newline at end of file diff --git a/example/finicky/finicky.cpp b/example/finicky/finicky.cpp new file mode 100644 index 00000000..f8c82f7c --- /dev/null +++ b/example/finicky/finicky.cpp @@ -0,0 +1,394 @@ +// +// Copyright (c) 2022 alandefreitas (alandefreitas@gmail.com) +// +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt +// + +//[example_finicky + +/* + This example shows how to classify URLs + according to a set of rules. This example is + inspired by Finicky. The URLs are classified + and redirected to a browser according to their + category. See the example config.json file. + https://github.com/johnste/finicky +*/ + +#include +#include +#include +#include +#include +#include + +namespace urls = boost::urls; +namespace json = boost::json; + +json::value +read_json( std::istream& is, json::error_code& ec ) +{ + json::parse_options opt; + opt.allow_comments = true; + json::stream_parser p(json::storage_ptr(), opt); + std::string line; + while( std::getline( is, line ) ) + { + p.write( line, ec ); + if( ec ) + return nullptr; + } + p.finish( ec ); + if( ec ) + return nullptr; + return p.release(); +} + +bool +glob_match( + urls::string_view pattern, + urls::string_view str) +{ + // regex + if (str.starts_with("/") && + str.ends_with("/")) + { + const boost::regex pr(pattern.begin() + 1, pattern.end() - 1); + return boost::regex_match(std::string(str), pr); + } + + // literal + if (!pattern.contains('*')) + { + return pattern == str; + } + + // glob + std::string p = pattern; + std::size_t i = p.find('*'); + while (i != std::string::npos) + { + auto e = std::min(p.find_first_not_of('*', i), p.size()); + std::size_t n = e - i; + if (n == 1) + { + p.replace(i, e, "[^/]*"); + i += 5; + } + else + { + p.replace(i, e, ".*"); + i += 2; + } + i = p.find('*', i); + } + const boost::regex pr(p); + return boost::regex_match(std::string(str), pr); +} + +bool +url_match( + json::value& mv, + urls::url const& u) +{ + if (mv.is_string()) + { + json::string& p = mv.as_string(); + return glob_match(u.buffer(), p); + } + else if (mv.is_array()) + { + json::array& m = mv.as_array(); + for (auto& mi: m) + { + if (!mi.is_string()) + throw std::invalid_argument( + "handle match is not a string"); + if (glob_match(mi.as_string(), u.buffer())) + return true; + } + } + else if (mv.is_object()) + { + json::object& m = mv.as_object(); + std::pair + field_values[] = { + {"protocol", u.scheme()}, + {"authority", u.encoded_authority()}, + {"username", u.encoded_user()}, + {"user", u.encoded_user()}, + {"password", u.encoded_password()}, + {"userinfo", u.encoded_userinfo()}, + {"host", u.encoded_host()}, + {"port", u.port()}, + {"path", u.encoded_path()}, + {"pathname", u.encoded_path()}, + {"query", u.encoded_query()}, + {"search", u.encoded_query()}, + {"fragment", u.encoded_fragment()}, + {"hash", u.encoded_fragment()}, + }; + for (auto& p: field_values) + { + auto it = m.find(p.first); + if (it != m.end()) + { + if (!it->value().is_string()) + throw std::invalid_argument( + "match fields should be a strings"); + if (glob_match(p.second, p.first)) + return true; + } + } + } + return false; +} + +#define CHECK(c, msg) \ + if (!(c)) \ + { \ + std::cerr << msg << "\n"; \ + return EXIT_FAILURE; \ + } + +int main(int argc, char** argv) +{ + if (argc < 3) { + std::cout << argv[0] << "\n"; + std::cout << "Usage: finicky \n" + "options:\n" + " : Configuration file\n" + " : The url to open\n" + "examples:\n" + " finicky config.json \"http://www.example.com\"\n"; + return EXIT_FAILURE; + } + + // Parse url + urls::result ru = urls::parse_uri(argv[2]); + CHECK(ru, "Invalid URL"); + urls::url u = *ru; + + // Open config file + std::fstream fin(argv[1]); + CHECK(fin.good(), "Cannot open configuration file"); + json::error_code ec; + json::value c = read_json(fin, ec); + CHECK(!ec.failed(), "Cannot parse configuration file"); + CHECK(c.is_object(), "Configuration file is not an object"); + json::object& o = c.as_object(); + + // Set initial browser + auto bit = o.find("defaultBrowser"); + CHECK( + bit != o.end(), + "Configuration file has no defaultBrowser"); + CHECK( + bit->value().is_string(), + "defaultBrowser should be a string"); + json::string& browser = bit->value().as_string(); + + // Apply rewrites to the input string + auto rsit = o.find("rewrite"); + if (rsit != o.end()) + { + CHECK( + rsit->value().is_array(), + "rewrite rules should be an array"); + auto& rs = rsit->value().as_array(); + for (auto& rv: rs) + { + CHECK( + rv.is_object(), + "individual rewrite rule should be an object"); + json::object& r = rv.as_object(); + + // Look for match + auto mit = r.find("match"); + CHECK( + mit != r.end(), + "rewrite rule should have a match field"); + CHECK( + mit->value().is_object() || mit->value().is_string(), + "rewrite match field is not an object"); + if (!url_match(mit->value(), u)) + continue; + + // Apply replacement rule + auto uit = r.find("url"); + CHECK( + uit != r.end(), + "rewrite rule should have a url field"); + CHECK( + uit->value().is_object() || + uit->value().is_string(), + "url field must be an object or string"); + + if (uit->value().is_string()) + { + json::string& uo = uit->value().as_string(); + auto ru1 = urls::parse_uri(uo); + CHECK(ru1, "url " << uo.c_str() << " is invalid"); + u = *ru; + } + else + { + json::object& uo = uit->value().as_object(); + auto it = uo.find("protocol"); + if (it != uo.end()) + { + CHECK( + it->value().is_string(), + "protocol field should be a string"); + u.set_scheme(it->value().as_string()); + } + + it = uo.find("authority"); + if (it != uo.end()) + { + CHECK( + it->value().is_string(), + "authority field should be a string"); + u.set_encoded_authority( + it->value().as_string()); + } + + it = uo.find("username"); + if (it == uo.end()) + it = uo.find("user"); + if (it != uo.end()) + { + CHECK( + it->value().is_string(), + "username field should be a string"); + u.set_encoded_user( + it->value().as_string()); + } + + it = uo.find("password"); + if (it != uo.end()) + { + CHECK( + it->value().is_string(), + "password field should be a string"); + u.set_encoded_password( + it->value().as_string()); + } + + it = uo.find("userinfo"); + if (it != uo.end()) + { + CHECK( + it->value().is_string(), + "userinfo field should be a string"); + u.set_encoded_userinfo( + it->value().as_string()); + } + + it = uo.find("host"); + if (it != uo.end()) + { + CHECK( + it->value().is_string(), + "host field should be a string"); + u.set_encoded_host( + it->value().as_string()); + } + + it = uo.find("port"); + if (it != uo.end()) + { + CHECK( + it->value().is_string(), + "port field should be a string"); + u.set_port( + it->value().as_string()); + } + + it = uo.find("path"); + if (it == uo.end()) + it = uo.find("pathname"); + if (it != uo.end()) + { + CHECK( + it->value().is_string(), + "path field should be a string"); + u.set_encoded_path( + it->value().as_string()); + } + + it = uo.find("query"); + if (it == uo.end()) + it = uo.find("search"); + if (it != uo.end()) + { + CHECK( + it->value().is_string(), + "query field should be a string"); + u.set_encoded_query( + it->value().as_string()); + } + + it = uo.find("fragment"); + if (it == uo.end()) + it = uo.find("hash"); + if (it != uo.end()) + { + CHECK( + it->value().is_string(), + "fragment field should be a string"); + u.set_encoded_fragment( + it->value().as_string()); + } + } + } + } + + // Determine which browser should handle the url + auto hsit = o.find("handlers"); + if (hsit != o.end()) + { + CHECK( + hsit->value().is_array(), + "handler rules should be an array"); + auto& hs = hsit->value().as_array(); + for (auto& hv: hs) + { + CHECK( + hv.is_object(), + "individual handlers should be an object"); + json::object& h = hv.as_object(); + + auto mit = h.find("match"); + CHECK( + mit != h.end(), + "handle rule should have a match field"); + CHECK( + mit->value().is_string() || mit->value().is_array(), + "handle match field must be an array or a string"); + + auto hbit = h.find("browser"); + CHECK( + hbit != h.end(), + "handle rule should have a browser field"); + CHECK( + hbit->value().is_string(), + "browser field is not a string"); + + // Look for match and change browser + if (url_match(mit->value(), u)) + { + browser = hbit->value().as_string(); + break; + } + } + } + + // Print command finicky would run + std::cout << "\"" << browser.c_str() << "\" " << u << '\n'; + + return EXIT_SUCCESS; +} + +//]