Initial commit
This commit is contained in:
		
						commit
						f3bb6ac62b
					
				|  | @ -0,0 +1 @@ | |||
| /build | ||||
|  | @ -0,0 +1,34 @@ | |||
| cmake_minimum_required(VERSION 3.16) | ||||
| 
 | ||||
| project(mf C CXX) | ||||
| 
 | ||||
| 
 | ||||
| find_package(SQLite3 REQUIRED) | ||||
| 
 | ||||
| 
 | ||||
| if (CMAKE_BUILD_TYPE MATCHES "Debug") | ||||
|     if (NOT FLAGS) | ||||
|         list(APPEND FLAGS -fsanitize=undefined) | ||||
|     endif() | ||||
|     # https://sourceforge.net/p/valgrind/mailman/valgrind-users/thread/Ygze8PzaQAYWlKDj%40wildebeest.org/ | ||||
|     list(APPEND FLAGS -gdwarf-4) | ||||
| endif() | ||||
| 
 | ||||
| # https://t.me/NightShadowsHangout/670691 | ||||
| list(APPEND FLAGS -Werror -Wall -Wextra -Wshadow -Wpedantic -Wno-gnu-anonymous-struct -fPIC -fno-rtti -Wconversion -Wno-unused-parameter -Wimplicit-fallthrough) | ||||
| 
 | ||||
| # i have no idea why this hack wasn't needed before but it's needed if sanitizers are used | ||||
| add_link_options(${FLAGS}) | ||||
| 
 | ||||
| 
 | ||||
| add_executable(${PROJECT_NAME} main.cpp sqlite_wrapper.cpp database.cpp utils.cpp blankie/cliparse.cpp | ||||
|     subcommand_create.cpp subcommand_search.cpp subcommand_prune.cpp | ||||
|     subcommand_info.cpp subcommand_delete.cpp subcommand_edit.cpp subcommand_get.cpp subcommand_set.cpp) | ||||
| set_target_properties(${PROJECT_NAME} | ||||
|     PROPERTIES | ||||
|         CXX_STANDARD 20 | ||||
|         CXX_STANDARD_REQUIRED YES | ||||
|         CXX_EXTENSIONS NO | ||||
| ) | ||||
| target_link_libraries(${PROJECT_NAME} PRIVATE SQLite::SQLite3) | ||||
| target_compile_options(${PROJECT_NAME} PRIVATE ${FLAGS}) | ||||
|  | @ -0,0 +1,19 @@ | |||
| Copyright (c) 2023 blankie | ||||
| 
 | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
| 
 | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
| 
 | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
|  | @ -0,0 +1,123 @@ | |||
| #include <algorithm> | ||||
| #include <cstring> | ||||
| 
 | ||||
| #include "cliparse.h" | ||||
| 
 | ||||
| namespace blankie { | ||||
| namespace cliparse { | ||||
| 
 | ||||
| void Parser::parse(int argc, char** argv) { | ||||
|     for (Flag& flag : this->flags) { | ||||
|         flag.used = false; | ||||
|         flag.argument = nullptr; | ||||
|     } | ||||
|     this->arguments.clear(); | ||||
| 
 | ||||
|     int offset = 0; | ||||
|     bool parse_flags = true; | ||||
| 
 | ||||
|     if (offset == 0 && argc) { | ||||
|         this->program_name = argv[0]; | ||||
|         offset++; | ||||
|     } | ||||
| 
 | ||||
|     while (argc > offset) { | ||||
|         if (argv[offset][0] != '-' || argv[offset][1] == '\0' || !parse_flags) { | ||||
|             this->arguments.push_back(argv[offset]); | ||||
|         } else if (argv[offset][1] == '-') { | ||||
|             this->_handle_long_flag(argc, argv, offset, parse_flags); | ||||
|         } else { | ||||
|             this->_handle_short_flags(argc, argv, offset); | ||||
|         } | ||||
| 
 | ||||
|         offset++; | ||||
|     } | ||||
| 
 | ||||
|     auto unused_flag = std::find_if(this->flags.begin(), this->flags.end(), [](Flag& flag) { | ||||
|         return flag.required && !flag.used; | ||||
|     }); | ||||
|     if (unused_flag != this->flags.end()) { | ||||
|         throw missing_flag(unused_flag->long_flag); | ||||
|     } | ||||
| 
 | ||||
|     if (this->arguments_min > this->arguments.size() || this->arguments_max < this->arguments.size()) { | ||||
|         throw invalid_argument_count(this->arguments_min, this->arguments_max); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void Parser::_handle_long_flag(int argc, char** argv, int& offset, bool& parse_flags) { | ||||
|     if (argv[offset][2] == '\0') { | ||||
|         parse_flags = false; | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const char* long_flag = &argv[offset][2]; | ||||
|     const char* equal_offset = strchr(long_flag, '='); | ||||
|     size_t long_flag_length = equal_offset ? static_cast<size_t>(equal_offset - long_flag) : strlen(long_flag); | ||||
|     auto flag = std::find_if(this->flags.begin(), this->flags.end(), [=](Flag& flag) { | ||||
|         return strncmp(flag.long_flag, long_flag, long_flag_length) == 0 && flag.long_flag[long_flag_length] == '\0'; | ||||
|     }); | ||||
| 
 | ||||
|     if (flag == this->flags.end()) { | ||||
|         throw unknown_flag(std::string(long_flag, long_flag_length)); | ||||
|     } | ||||
|     flag->used = true; | ||||
| 
 | ||||
|     if (flag->argument_required == ArgumentRequired::No) { | ||||
|         if (equal_offset) { | ||||
|             throw unnecessary_argument(flag->long_flag); | ||||
|         } | ||||
|     } else if (equal_offset) { | ||||
|         flag->argument = &equal_offset[1]; | ||||
|     } else if (flag->argument_required == ArgumentRequired::Yes) { | ||||
|         if (offset < argc - 1) { | ||||
|             flag->argument = argv[++offset]; | ||||
|         } else { | ||||
|             throw missing_argument(flag->long_flag); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| void Parser::_handle_short_flags(int argc, char** argv, int& offset) { | ||||
|     const char* options = &argv[offset][1]; | ||||
| 
 | ||||
|     while (options[0] != '\0') { | ||||
|         Flag* flag = this->flag(options[0]); | ||||
|         if (!flag) { | ||||
|             throw unknown_flag(options[0]); | ||||
|         } | ||||
| 
 | ||||
|         options++; | ||||
|         flag->used = true; | ||||
|         if (flag->argument_required == ArgumentRequired::No) { | ||||
|             continue; | ||||
|         } else if (options[0] != '\0') { | ||||
|             flag->argument = options[0] == '=' ? &options[1] : options; | ||||
|             break; | ||||
|         } else if (flag->argument_required == ArgumentRequired::Yes) { | ||||
|             if (offset < argc - 1) { | ||||
|                 flag->argument = argv[++offset]; | ||||
|             } else { | ||||
|                 throw missing_argument(flag->long_flag); | ||||
|             } | ||||
|             break; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| Flag* Parser::flag(const char* long_flag) { | ||||
|     auto flag = std::find_if(this->flags.begin(), this->flags.end(), [=](Flag& flag) { | ||||
|         return strcmp(flag.long_flag, long_flag) == 0; | ||||
|     }); | ||||
|     return flag != this->flags.end() ? &*flag : nullptr; | ||||
| } | ||||
| Flag* Parser::flag(char short_flag) { | ||||
|     auto flag = std::find_if(this->flags.begin(), this->flags.end(), [=](Flag& flag) { | ||||
|         return flag.short_flag == short_flag; | ||||
|     }); | ||||
|     return flag != this->flags.end() ? &*flag : nullptr; | ||||
| } | ||||
| 
 | ||||
| } // namespace cliparse
 | ||||
| } // namespace blankie
 | ||||
|  | @ -0,0 +1,142 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <string> | ||||
| #include <vector> | ||||
| #include <cassert> | ||||
| #include <cstdint> | ||||
| #include <exception> | ||||
| 
 | ||||
| namespace blankie { | ||||
| namespace cliparse { | ||||
| 
 | ||||
| enum ArgumentRequired { | ||||
|     Yes, | ||||
|     Maybe, | ||||
|     No, | ||||
| }; | ||||
| 
 | ||||
| struct Flag { | ||||
|     const char* long_flag; // must not be null
 | ||||
|     char short_flag; // may be '\0'
 | ||||
|     bool required; | ||||
|     ArgumentRequired argument_required; | ||||
| 
 | ||||
|     bool used; | ||||
|     const char* argument; // may be null
 | ||||
| 
 | ||||
|     Flag(const char* long_flag_, char short_flag_, bool required_ = false, ArgumentRequired argument_required_ = ArgumentRequired::No) | ||||
|             : long_flag(long_flag_), short_flag(short_flag_), required(required_), argument_required(argument_required_) { | ||||
|         assert(this->long_flag != nullptr); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| struct Parser { | ||||
|     const char* program_name; // may be null
 | ||||
|     std::vector<Flag> flags; | ||||
|     size_t arguments_min; | ||||
|     size_t arguments_max; | ||||
|     std::vector<const char*> arguments; | ||||
| 
 | ||||
|     void parse(int argc, char** argv); | ||||
| 
 | ||||
|     Flag* flag(const char* long_flag); | ||||
|     Flag* flag(char short_flag); | ||||
| 
 | ||||
|     Parser(const char* program_name_, std::vector<Flag> flags_, size_t arguments_min_ = 0, size_t arguments_max_ = SIZE_MAX) | ||||
|             : program_name(program_name_), flags(std::move(flags_)), arguments_min(arguments_min_), arguments_max(arguments_max_) { | ||||
|         assert(this->arguments_max >= this->arguments_min); | ||||
|     } | ||||
| 
 | ||||
| private: | ||||
|     void _handle_long_flag(int argc, char** argv, int& offset, bool& parse_flags); | ||||
|     void _handle_short_flags(int argc, char** argv, int& offset); | ||||
| }; | ||||
| 
 | ||||
| class exception : public std::exception {}; | ||||
| 
 | ||||
| class missing_flag : public exception { | ||||
| public: | ||||
|     missing_flag(const char* long_flag_) : long_flag(long_flag_) { | ||||
|         using namespace std::string_literals; | ||||
|         this->_msg = "flag --"s + this->long_flag + " required, but not used"; | ||||
|     } | ||||
|     const char* what() const noexcept { | ||||
|         return this->_msg.c_str(); | ||||
|     } | ||||
| 
 | ||||
|     const char* long_flag; | ||||
| 
 | ||||
| private: | ||||
|     std::string _msg; | ||||
| }; | ||||
| 
 | ||||
| class missing_argument : public exception { | ||||
| public: | ||||
|     missing_argument(const char* long_flag_) : long_flag(long_flag_) { | ||||
|         using namespace std::string_literals; | ||||
|         this->_msg = "flag --"s + this->long_flag + " requires an argument, but one is not passed"; | ||||
|     } | ||||
|     const char* what() const noexcept { | ||||
|         return this->_msg.c_str(); | ||||
|     } | ||||
| 
 | ||||
|     const char* long_flag; | ||||
| 
 | ||||
| private: | ||||
|     std::string _msg; | ||||
| }; | ||||
| 
 | ||||
| class invalid_argument_count : public exception { | ||||
| public: | ||||
|     invalid_argument_count(size_t arguments_min_, size_t arguments_max_) : arguments_min(arguments_min_), arguments_max(arguments_max_) { | ||||
|         using namespace std::string_literals; | ||||
|         this->_msg = "too little or too many arguments provided, there must be "s | ||||
|             + std::to_string(this->arguments_min) + " to " + std::to_string(this->arguments_max) + " arguments"; | ||||
|     } | ||||
|     const char* what() const noexcept { | ||||
|         return this->_msg.c_str(); | ||||
|     } | ||||
| 
 | ||||
|     size_t arguments_min; | ||||
|     size_t arguments_max; | ||||
| 
 | ||||
| private: | ||||
|     std::string _msg; | ||||
| }; | ||||
| 
 | ||||
| class unknown_flag : public exception { | ||||
| public: | ||||
|     unknown_flag(std::string long_flag) { | ||||
|         using namespace std::string_literals; | ||||
|         this->_msg = "unknown flag --"s + std::move(long_flag); | ||||
|     } | ||||
|     unknown_flag(char short_flag) { | ||||
|         using namespace std::string_literals; | ||||
|         this->_msg = "unknown flag -"s + short_flag; | ||||
|     } | ||||
|     const char* what() const noexcept { | ||||
|         return this->_msg.c_str(); | ||||
|     } | ||||
| 
 | ||||
| private: | ||||
|     std::string _msg; | ||||
| }; | ||||
| 
 | ||||
| class unnecessary_argument : public exception { | ||||
| public: | ||||
|     unnecessary_argument(const char* long_flag_) : long_flag(long_flag_) { | ||||
|         using namespace std::string_literals; | ||||
|         this->_msg = "flag --"s + this->long_flag + " does not take in an argument, yet one is passed"; | ||||
|     } | ||||
|     const char* what() const noexcept { | ||||
|         return this->_msg.c_str(); | ||||
|     } | ||||
| 
 | ||||
|     const char* long_flag; | ||||
| 
 | ||||
| private: | ||||
|     std::string _msg; | ||||
| }; | ||||
| 
 | ||||
| } // namespace cliparse
 | ||||
| } // namespace blankie
 | ||||
|  | @ -0,0 +1,17 @@ | |||
| #include <cstdio> | ||||
| 
 | ||||
| #include "database.h" | ||||
| 
 | ||||
| Database Database::find(bool readonly) { | ||||
|     std::filesystem::path directory = std::filesystem::current_path(); | ||||
|     do { | ||||
|         std::filesystem::path path = directory / "mf.db"; | ||||
|         if (std::filesystem::is_regular_file(path)) { | ||||
|             return Database(path, readonly, false); | ||||
|         } | ||||
|         directory = directory.parent_path(); | ||||
|     } while (directory != directory.root_path()); | ||||
| 
 | ||||
|     fprintf(stderr, "Failed to find a database; have you created one and are in a directory with it?\n"); | ||||
|     exit(1); | ||||
| } | ||||
|  | @ -0,0 +1,17 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <filesystem> | ||||
| 
 | ||||
| #include "sqlite_wrapper.h" | ||||
| 
 | ||||
| struct Database { | ||||
|     Database(std::filesystem::path path_, bool readonly, bool create) : path(std::move(path_)), db(this->path.c_str(), readonly, create) {} | ||||
| 
 | ||||
|     static Database find(bool readonly); | ||||
|     static Database create_in_cwd() { | ||||
|         return Database(std::filesystem::current_path() / "mf.db", false, true); | ||||
|     } | ||||
| 
 | ||||
|     std::filesystem::path path; | ||||
|     Sqlite3 db; | ||||
| }; | ||||
|  | @ -0,0 +1,91 @@ | |||
| #include <cstdio> | ||||
| #include <cstdlib> | ||||
| #include <cstring> | ||||
| #include <unistd.h> | ||||
| 
 | ||||
| #include "database.h" | ||||
| #include "subcommands.h" | ||||
| #include "blankie/cliparse.h" | ||||
| 
 | ||||
| void sqlite_error_logger(void*, int error_code, const char* message) { | ||||
|     // For some reason, Sqlite3Statement::bind_text causes this
 | ||||
|     if (error_code == SQLITE_SCHEMA) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     fprintf(stderr, "sqlite: (%d) %s\n", error_code, message); | ||||
| } | ||||
| 
 | ||||
| void exit_handler() { | ||||
|     sqlite3_shutdown(); | ||||
| } | ||||
| 
 | ||||
| void real_main(int argc, char** argv); | ||||
| 
 | ||||
| int main(int argc, char** argv) { | ||||
|     sqlite3_config(SQLITE_CONFIG_LOG, sqlite_error_logger, nullptr); | ||||
| 
 | ||||
|     int error_code = sqlite3_initialize(); | ||||
|     if (error_code != SQLITE_OK) { | ||||
|         throw Sqlite3Exception(error_code, nullptr); | ||||
|     } | ||||
| 
 | ||||
|     atexit(exit_handler); | ||||
| 
 | ||||
|     try { | ||||
|         real_main(argc, argv); | ||||
|     } catch (...) { | ||||
|         sqlite3_shutdown(); | ||||
|         throw; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| void real_main(int argc, char** argv) { | ||||
|     Parser parser("mf", { | ||||
|         {"directory", 'C', false, blankie::cliparse::ArgumentRequired::Yes}, | ||||
|     }, 1, ~static_cast<size_t>(0)); | ||||
| 
 | ||||
|     try { | ||||
|         parser.parse(argc, argv); | ||||
|     } catch (const blankie::cliparse::invalid_argument_count& e) { | ||||
|         fprintf(stderr, HELP_TEXT, parser.program_name); | ||||
|         exit(1); | ||||
|     } catch (const blankie::cliparse::exception& e) { | ||||
|         fprintf(stderr, "failed to parse arguments: %s\n", e.what()); | ||||
|         exit(1); | ||||
|     } | ||||
| 
 | ||||
|     if (parser.flag('C')->used) { | ||||
|         if (chdir(parser.flag('C')->argument)) { | ||||
|             perror("failed to chdir"); | ||||
|             exit(1); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     auto is = [&](const char* subcommand) { | ||||
|         return strcmp(parser.arguments[0], subcommand) == 0; | ||||
|     }; | ||||
| 
 | ||||
|     if (is("create")) { | ||||
|         subcommand_create(parser); | ||||
|     } else if (is("search")) { | ||||
|         subcommand_search(parser); | ||||
|     } else if (is("prune")) { | ||||
|         subcommand_prune(parser); | ||||
|     } else if (is("info") || is("show")) { | ||||
|         subcommand_info(parser); | ||||
|     } else if (is("delete")) { | ||||
|         subcommand_delete(parser); | ||||
|     } else if (is("edit")) { | ||||
|         subcommand_edit(parser); | ||||
|     } else if (is("get")) { | ||||
|         subcommand_get(parser); | ||||
|     } else if (is("set")) { | ||||
|         subcommand_set(parser); | ||||
|     } else { | ||||
|         fprintf(stderr, HELP_TEXT, parser.program_name); | ||||
|         exit(1); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,24 @@ | |||
| #include "sqlite_wrapper.h" | ||||
| 
 | ||||
| Sqlite3::Sqlite3(const char* filename, bool readonly, bool create) { | ||||
|     int flags = readonly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE; | ||||
|     if (create) { | ||||
|         flags |= SQLITE_OPEN_CREATE; | ||||
|     } | ||||
| 
 | ||||
|     int error_code = sqlite3_open_v2(filename, &this->_db, flags, nullptr); | ||||
|     if (error_code != SQLITE_OK) { | ||||
|         Sqlite3Exception e(error_code, this); | ||||
|         sqlite3_close_v2(this->_db); | ||||
|         throw e; | ||||
|     } | ||||
| 
 | ||||
|     sqlite3_extended_result_codes(this->_db, 1); | ||||
| } | ||||
| 
 | ||||
| Sqlite3Statement::Sqlite3Statement(Sqlite3& db, const char* sql) { | ||||
|     int error_code = sqlite3_prepare_v2(db.get(), sql, -1, &this->_stmt, nullptr); | ||||
|     if (error_code != SQLITE_OK) { | ||||
|         throw Sqlite3Exception(error_code, &db); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,119 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <string> | ||||
| #include <exception> | ||||
| #include <sqlite3.h> | ||||
| 
 | ||||
| class Sqlite3Statement; | ||||
| 
 | ||||
| class Sqlite3 { | ||||
| public: | ||||
|     Sqlite3(const Sqlite3&) = delete; | ||||
|     Sqlite3& operator=(const Sqlite3&) = delete; | ||||
| 
 | ||||
|     Sqlite3(const char* filename, bool readonly, bool create); | ||||
|     ~Sqlite3() { | ||||
|         sqlite3_close_v2(this->_db); | ||||
|     } | ||||
| 
 | ||||
|     // https://stackoverflow.com/a/6458689
 | ||||
|     template<typename F> | ||||
|     constexpr void exec(Sqlite3Statement& stmt, F callback, size_t iterations = ~static_cast<size_t>(0)); | ||||
|     constexpr void exec(Sqlite3Statement& stmt) { | ||||
|         this->exec(stmt, []() {}); | ||||
|     } | ||||
| 
 | ||||
|     constexpr int64_t changes() const noexcept { | ||||
|         return sqlite3_changes64(this->_db); | ||||
|     } | ||||
| 
 | ||||
|     constexpr sqlite3* get() const noexcept { | ||||
|         return this->_db; | ||||
|     } | ||||
| 
 | ||||
| private: | ||||
|     sqlite3* _db; | ||||
| }; | ||||
| 
 | ||||
| class Sqlite3Statement { | ||||
| public: | ||||
|     Sqlite3Statement(const Sqlite3Statement&) = delete; | ||||
|     Sqlite3Statement& operator=(const Sqlite3Statement&) = delete; | ||||
| 
 | ||||
|     Sqlite3Statement(Sqlite3& db, const char* sql); | ||||
|     ~Sqlite3Statement() { | ||||
|         sqlite3_finalize(this->_stmt); | ||||
|     } | ||||
| 
 | ||||
|     constexpr void reset() noexcept { | ||||
|         sqlite3_reset(this->_stmt); | ||||
|     } | ||||
| 
 | ||||
|     constexpr void bind_text(int column, const char* text, int bytes, void (*lifetime)(void*)); | ||||
|     constexpr void bind_text(int column, const char* text, void (*lifetime)(void*)) { | ||||
|         this->bind_text(column, text, -1, lifetime); | ||||
|     } | ||||
|     constexpr void bind_text(int column, const std::string& text, void (*lifetime)(void*)) { | ||||
|         this->bind_text(column, text.data(), static_cast<int>(text.size()), lifetime); | ||||
|     } | ||||
| 
 | ||||
|     constexpr void bind_null(int column); | ||||
| 
 | ||||
|     const char* column_text(int column) const noexcept { | ||||
|         return reinterpret_cast<const char*>(sqlite3_column_text(this->_stmt, column)); | ||||
|     } | ||||
|     constexpr int64_t column_int64(int column) const noexcept { | ||||
|         return sqlite3_column_int64(this->_stmt, column); | ||||
|     } | ||||
| 
 | ||||
|     friend Sqlite3; | ||||
| 
 | ||||
| private: | ||||
|     sqlite3_stmt* _stmt; | ||||
| }; | ||||
| 
 | ||||
| class Sqlite3Exception : public std::exception { | ||||
| public: | ||||
|     Sqlite3Exception(int error_code, const Sqlite3* db) { | ||||
|         this->_msg = db && db->get() | ||||
|             ? sqlite3_errmsg(db->get()) | ||||
|             : sqlite3_errstr(error_code); | ||||
|     } | ||||
| 
 | ||||
|     const char* what() const noexcept { | ||||
|         return this->_msg.c_str(); | ||||
|     } | ||||
| 
 | ||||
| private: | ||||
|     std::string _msg; | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| template<typename F> | ||||
| constexpr void Sqlite3::exec(Sqlite3Statement& stmt, F callback, size_t iterations) { | ||||
|     while (iterations--) { | ||||
|         int error_code = sqlite3_step(stmt._stmt); | ||||
|         if (error_code == SQLITE_DONE) { | ||||
|             break; | ||||
|         } else if (error_code == SQLITE_ROW) { | ||||
|             callback(); | ||||
|         } else if (error_code != SQLITE_OK) { | ||||
|             throw Sqlite3Exception(error_code, this); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| constexpr void Sqlite3Statement::bind_text(int column, const char* text, int bytes, void (*lifetime)(void*)) { | ||||
|     int error_code = sqlite3_bind_text(this->_stmt, column, text, bytes, lifetime); | ||||
|     if (error_code != SQLITE_OK) { | ||||
|         throw Sqlite3Exception(error_code, nullptr); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| constexpr void Sqlite3Statement::bind_null(int column) { | ||||
|     int error_code = sqlite3_bind_null(this->_stmt, column); | ||||
|     if (error_code != SQLITE_OK) { | ||||
|         throw Sqlite3Exception(error_code, nullptr); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,21 @@ | |||
| #include "database.h" | ||||
| 
 | ||||
| #include "subcommands.h" | ||||
| 
 | ||||
| void subcommand_create(const Parser& parser) { | ||||
|     if (parser.arguments.size() != 1) { | ||||
|         fprintf(stderr, HELP_TEXT, parser.program_name); | ||||
|         exit(1); | ||||
|     } | ||||
| 
 | ||||
|     Database db = Database::create_in_cwd(); | ||||
| 
 | ||||
|     Sqlite3Statement stmt(db.db, R"EOF(CREATE VIRTUAL TABLE IF NOT EXISTS memes USING fts4( | ||||
|         path TEXT PRIMARY KEY NOT NULL, | ||||
|         source TEXT NOT NULL, | ||||
|         description TEXT NOT NULL, | ||||
|         miscinfo TEXT NOT NULL, | ||||
|         matchinfo=fts3 | ||||
|     ))EOF"); | ||||
|     db.db.exec(stmt); | ||||
| } | ||||
|  | @ -0,0 +1,29 @@ | |||
| #include "utils.h" | ||||
| #include "database.h" | ||||
| 
 | ||||
| #include "subcommands.h" | ||||
| 
 | ||||
| void subcommand_delete(const Parser& parser) { | ||||
|     if (parser.arguments.size() < 2) { | ||||
|         fprintf(stderr, HELP_TEXT, parser.program_name); | ||||
|         exit(1); | ||||
|     } | ||||
| 
 | ||||
|     int rc = 0; | ||||
|     Database db = Database::find(false); | ||||
|     std::vector<std::filesystem::path> paths = get_paths_relative_to_database(parser, db, &rc, false); | ||||
|     Sqlite3Statement stmt(db.db, "DELETE FROM memes WHERE path = ?"); | ||||
| 
 | ||||
|     for (const std::filesystem::path& i : paths) { | ||||
|         stmt.reset(); | ||||
|         stmt.bind_text(1, i.native(), SQLITE_STATIC); | ||||
|         db.db.exec(stmt); | ||||
| 
 | ||||
|         if (db.db.changes() <= 0) { | ||||
|             fprintf(stderr, "failed to delete %s: no such file\n", i.c_str()); | ||||
|             rc = 1; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     exit(rc); | ||||
| } | ||||
|  | @ -0,0 +1,150 @@ | |||
| #include <regex> | ||||
| #include <system_error> | ||||
| #include <unistd.h> | ||||
| #include <sys/wait.h> | ||||
| 
 | ||||
| #include "utils.h" | ||||
| #include "database.h" | ||||
| 
 | ||||
| #include "subcommands.h" | ||||
| 
 | ||||
| using namespace std::string_literals; | ||||
| 
 | ||||
| const std::regex contents_regex("# Path \\(immutable\\):\n\\s*[^\0]*?\\s*\0\n# Source:\n\\s*([^\0]*?)\\s*\0\n# Description:\n\\s*([^\0]*?)\\s*\0\n# Miscellaneous information:\n\\s*([^\0]*?)\\s*"s); | ||||
| static inline const char* editor(); | ||||
| static inline std::string read_file(int fd); | ||||
| 
 | ||||
| struct TemporaryFile { | ||||
|     int fd; | ||||
|     char name[14] = "/tmp/mfXXXXXX"; | ||||
|     bool unlinked = false; | ||||
| 
 | ||||
|     TemporaryFile() { | ||||
|         this->fd = mkstemp(this->name); | ||||
|         if (this->fd == -1) { | ||||
|             throw std::system_error(errno, std::generic_category(), "mkstemp()"); | ||||
|         } | ||||
|     } | ||||
|     ~TemporaryFile() { | ||||
|         if (close(this->fd)) { | ||||
|             perror("close(temp_fd)"); | ||||
|         } | ||||
|         this->unlink(); | ||||
|     } | ||||
| 
 | ||||
|     void unlink() { | ||||
|         if (!this->unlinked && ::unlink(this->name)) { | ||||
|             perror("unlink(temp_name)"); | ||||
|         } | ||||
|         this->unlinked = true; | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| void subcommand_edit(const Parser& parser) { | ||||
|     // Sanity check the arguments passed
 | ||||
|     if (parser.arguments.size() != 2) { | ||||
|         fprintf(stderr, HELP_TEXT, parser.program_name); | ||||
|         exit(1); | ||||
|     } | ||||
| 
 | ||||
|     // Open the database and find the meme's path, relative to the database
 | ||||
|     Database db = Database::find(false); | ||||
|     std::filesystem::path path = get_path_relative_to_database(parser.arguments[1], db); | ||||
| 
 | ||||
|     // Select the meme, if any
 | ||||
|     bool meme_found = false; | ||||
|     Sqlite3Statement select_stmt(db.db, "SELECT source, description, miscinfo FROM memes WHERE path = ?"); | ||||
|     select_stmt.bind_text(1, path.native(), SQLITE_STATIC); | ||||
|     db.db.exec(select_stmt, [&]() { | ||||
|         meme_found = true; | ||||
|     }, 1); | ||||
| 
 | ||||
|     // Create a temporary file and write the meme's metadata to it
 | ||||
|     TemporaryFile tempfile; | ||||
|     output_meme( | ||||
|         path.c_str(), | ||||
|         meme_found ? select_stmt.column_text(0) : "", | ||||
|         meme_found ? select_stmt.column_text(1) : "", | ||||
|         meme_found ? select_stmt.column_text(2) : "", | ||||
|         tempfile.fd, | ||||
|         true | ||||
|     ); | ||||
|     select_stmt.reset(); | ||||
|     select_stmt.bind_null(1); | ||||
|     if (lseek(tempfile.fd, 0, SEEK_SET)) { | ||||
|         perror("lseek(temp_fd)"); | ||||
|         exit(1); | ||||
|     } | ||||
| 
 | ||||
|     // Spawn $VISUAL/$EDITOR/vi
 | ||||
|     pid_t editor_pid = fork(); | ||||
|     if (editor_pid == -1) { | ||||
|         perror("fork()"); | ||||
|         exit(1); | ||||
|     } else if (editor_pid == 0) { | ||||
|         execlp(editor(), editor(), tempfile.name, nullptr); | ||||
|         perror("execlp()"); | ||||
|         _exit(1); | ||||
|     } else if (waitpid(editor_pid, nullptr, 0) != editor_pid) { | ||||
|         perror("waitpid()"); | ||||
|     } | ||||
| 
 | ||||
|     // Delete the temporary file and read its contents
 | ||||
|     tempfile.unlink(); | ||||
|     std::string contents = read_file(tempfile.fd); | ||||
|     std::smatch sm; | ||||
|     if (!std::regex_match(contents, sm, contents_regex)) { | ||||
|         fprintf(stderr, "failed to parse file contents\n"); | ||||
|         exit(1); | ||||
|     } | ||||
| 
 | ||||
|     // Write new meme metadata to the database
 | ||||
|     std::string source = sm.str(1); | ||||
|     std::string description = sm.str(2); | ||||
|     std::string miscinfo = sm.str(3); | ||||
|     if (meme_found) { | ||||
|         Sqlite3Statement update_stmt(db.db, "UPDATE memes SET source = ?, description = ?, miscinfo = ? WHERE path = ?"); | ||||
|         update_stmt.bind_text(1, source, SQLITE_STATIC); | ||||
|         update_stmt.bind_text(2, description, SQLITE_STATIC); | ||||
|         update_stmt.bind_text(3, miscinfo, SQLITE_STATIC); | ||||
|         update_stmt.bind_text(4, path.native(), SQLITE_STATIC); | ||||
|         db.db.exec(update_stmt); | ||||
|     } else { | ||||
|         Sqlite3Statement insert_stmt(db.db, "INSERT INTO memes (path, source, description, miscinfo) VALUES (?, ?, ?, ?)"); | ||||
|         insert_stmt.bind_text(1, path.native(), SQLITE_STATIC); | ||||
|         insert_stmt.bind_text(2, source, SQLITE_STATIC); | ||||
|         insert_stmt.bind_text(3, description, SQLITE_STATIC); | ||||
|         insert_stmt.bind_text(4, miscinfo, SQLITE_STATIC); | ||||
|         db.db.exec(insert_stmt); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| static inline const char* editor() { | ||||
|     if (getenv("VISUAL")) { | ||||
|         return getenv("VISUAL"); | ||||
|     } | ||||
|     if (getenv("EDITOR")) { | ||||
|         return getenv("EDITOR"); | ||||
|     } | ||||
|     return "vi"; | ||||
| } | ||||
| 
 | ||||
| static inline std::string read_file(int fd) { | ||||
|     std::string res; | ||||
|     char buf[4096]; | ||||
| 
 | ||||
|     while (true) { | ||||
|         ssize_t read_size = read(fd, buf, 4096); | ||||
|         if (read_size == -1) { | ||||
|             throw std::system_error(errno, std::generic_category(), "read(temp_fd)"); | ||||
|         } else if (read_size == 0) { | ||||
|             break; | ||||
|         } else { | ||||
|             res.insert(res.end(), buf, &buf[read_size]); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return res; | ||||
| } | ||||
|  | @ -0,0 +1,31 @@ | |||
| #include "utils.h" | ||||
| #include "database.h" | ||||
| 
 | ||||
| #include "subcommands.h" | ||||
| 
 | ||||
| void subcommand_get(const Parser& parser) { | ||||
|     // Sanity check the arguments passed
 | ||||
|     if (parser.arguments.size() != 2) { | ||||
|         fprintf(stderr, HELP_TEXT, parser.program_name); | ||||
|         exit(1); | ||||
|     } | ||||
| 
 | ||||
|     // Open the database and find the meme's path, relative to the database
 | ||||
|     Database db = Database::find(true); | ||||
|     std::filesystem::path path = get_path_relative_to_database(parser.arguments[1], db); | ||||
| 
 | ||||
|     // Select the meme, if any
 | ||||
|     bool meme_found = false; | ||||
|     Sqlite3Statement select_stmt(db.db, "SELECT source, description, miscinfo FROM memes WHERE path = ?"); | ||||
|     select_stmt.bind_text(1, path.native(), SQLITE_STATIC); | ||||
|     db.db.exec(select_stmt, [&]() { | ||||
|         meme_found = true; | ||||
|     }, 1); | ||||
| 
 | ||||
|     // Output the meme's metadata to stdout
 | ||||
|     printf("%s%c%s%c%s%c%s", | ||||
|         path.c_str(), 0, | ||||
|         meme_found ? select_stmt.column_text(0) : "", 0, | ||||
|         meme_found ? select_stmt.column_text(1) : "", 0, | ||||
|         meme_found ? select_stmt.column_text(2) : ""); | ||||
| } | ||||
|  | @ -0,0 +1,42 @@ | |||
| #include <unistd.h> | ||||
| 
 | ||||
| #include "utils.h" | ||||
| #include "database.h" | ||||
| 
 | ||||
| #include "subcommands.h" | ||||
| 
 | ||||
| void subcommand_info(const Parser& parser) { | ||||
|     if (parser.arguments.size() < 2) { | ||||
|         fprintf(stderr, HELP_TEXT, parser.program_name); | ||||
|         exit(1); | ||||
|     } | ||||
| 
 | ||||
|     int rc = 0; | ||||
|     bool first_item = true; | ||||
|     Database db = Database::find(true); | ||||
|     std::vector<std::filesystem::path> paths = get_paths_relative_to_database(parser, db, &rc); | ||||
|     Sqlite3Statement stmt(db.db, "SELECT source, description, miscinfo FROM memes WHERE path = ?"); | ||||
| 
 | ||||
|     for (const std::filesystem::path& i : paths) { | ||||
|         bool item_found = false; | ||||
| 
 | ||||
|         stmt.reset(); | ||||
|         stmt.bind_text(1, i.native(), SQLITE_STATIC); | ||||
|         db.db.exec(stmt, [&]() { | ||||
|             if (!first_item) { | ||||
|                 write(1, "================================================================================\n", 81); | ||||
|             } | ||||
|             first_item = false; | ||||
|             item_found = true; | ||||
| 
 | ||||
|             output_meme(i.c_str(), stmt.column_text(0), stmt.column_text(1), stmt.column_text(2)); | ||||
|         }); | ||||
| 
 | ||||
|         if (!item_found) { | ||||
|             fprintf(stderr, "failed to find %s\n", i.c_str()); | ||||
|             rc = 1; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     exit(first_item ? 1 : rc); | ||||
| } | ||||
|  | @ -0,0 +1,38 @@ | |||
| #include "database.h" | ||||
| 
 | ||||
| #include "subcommands.h" | ||||
| 
 | ||||
| static inline void is_file(sqlite3_context* context, int argc, sqlite3_value** argv); | ||||
| 
 | ||||
| void subcommand_prune(const Parser& parser) { | ||||
|     if (parser.arguments.size() != 1) { | ||||
|         fprintf(stderr, HELP_TEXT, parser.program_name); | ||||
|         exit(1); | ||||
|     } | ||||
| 
 | ||||
|     Database db = Database::find(false); | ||||
| 
 | ||||
|     // https://stackoverflow.com/a/4929486
 | ||||
|     int error_code = sqlite3_create_function(db.db.get(), "is_file", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC | SQLITE_DIRECTONLY, &db.path, is_file, nullptr, nullptr); | ||||
|     if (error_code != SQLITE_OK) { | ||||
|         throw Sqlite3Exception(error_code, &db.db); | ||||
|     } | ||||
|     Sqlite3Statement delete_stmt(db.db, "DELETE FROM memes WHERE NOT is_file(path)"); | ||||
|     db.db.exec(delete_stmt); | ||||
|     sqlite3_create_function(db.db.get(), "is_file", 1, SQLITE_UTF8 | SQLITE_DETERMINISTIC | SQLITE_DIRECTONLY, &db.path, nullptr, nullptr, nullptr); | ||||
| 
 | ||||
|     Sqlite3Statement vacuum_stmt(db.db, "VACUUM"); | ||||
|     db.db.exec(vacuum_stmt); | ||||
| } | ||||
| 
 | ||||
| static inline void is_file(sqlite3_context* context, int argc, sqlite3_value** argv) { | ||||
|     if (argc != 1) { | ||||
|         sqlite3_result_error(context, "is_file() expects one argument", -1); | ||||
|         sqlite3_result_error_code(context, SQLITE_MISUSE); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     const std::filesystem::path* db_path = static_cast<const std::filesystem::path*>(sqlite3_user_data(context)); | ||||
|     std::filesystem::path file_path = db_path->parent_path() / reinterpret_cast<const char*>(sqlite3_value_text(argv[0])); | ||||
|     sqlite3_result_int(context, std::filesystem::is_regular_file(file_path)); | ||||
| } | ||||
|  | @ -0,0 +1,35 @@ | |||
| #include <unistd.h> | ||||
| 
 | ||||
| #include "utils.h" | ||||
| #include "database.h" | ||||
| 
 | ||||
| #include "subcommands.h" | ||||
| 
 | ||||
| void subcommand_search(const Parser& parser) { | ||||
|     if (parser.arguments.size() != 2) { | ||||
|         fprintf(stderr, HELP_TEXT, parser.program_name); | ||||
|         exit(1); | ||||
|     } | ||||
| 
 | ||||
|     Database db = Database::find(true); | ||||
|     Sqlite3Statement stmt(db.db, "SELECT path, source, description, miscinfo FROM memes WHERE memes MATCH ?"); | ||||
|     bool first_item = true; | ||||
| 
 | ||||
|     stmt.bind_text(1, parser.arguments[1], SQLITE_STATIC); | ||||
|     try { | ||||
|         db.db.exec(stmt, [&]() { | ||||
|             if (!first_item) { | ||||
|                 write(1, "================================================================================\n", 81); | ||||
|             } | ||||
|             first_item = false; | ||||
| 
 | ||||
|             output_meme(stmt.column_text(0), stmt.column_text(1), stmt.column_text(2), stmt.column_text(3)); | ||||
|         }); | ||||
|     } catch (const Sqlite3Exception& e) { | ||||
|         // it'll be printed to the error log, so do nothing
 | ||||
|     } | ||||
| 
 | ||||
|     if (first_item) { | ||||
|         exit(1); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,34 @@ | |||
| #include "utils.h" | ||||
| #include "database.h" | ||||
| 
 | ||||
| #include "subcommands.h" | ||||
| 
 | ||||
| void subcommand_set(const Parser& parser) { | ||||
|     // Sanity check the arguments passed
 | ||||
|     if (parser.arguments.size() != 5) { | ||||
|         fprintf(stderr, HELP_TEXT, parser.program_name); | ||||
|         exit(1); | ||||
|     } | ||||
| 
 | ||||
|     // Open the database and find the meme's path, relative to the database
 | ||||
|     Database db = Database::find(false); | ||||
|     std::filesystem::path path = get_path_relative_to_database(parser.arguments[1], db); | ||||
| 
 | ||||
|     // Update an existing meme, if any
 | ||||
|     Sqlite3Statement update_stmt(db.db, "UPDATE memes SET source = ?, description = ?, miscinfo = ? WHERE path = ?"); | ||||
|     update_stmt.bind_text(1, parser.arguments[2], SQLITE_STATIC); | ||||
|     update_stmt.bind_text(2, parser.arguments[3], SQLITE_STATIC); | ||||
|     update_stmt.bind_text(3, parser.arguments[4], SQLITE_STATIC); | ||||
|     update_stmt.bind_text(4, path.native(), SQLITE_STATIC); | ||||
|     db.db.exec(update_stmt); | ||||
| 
 | ||||
|     // If there is not one, then create one
 | ||||
|     if (db.db.changes() <= 0) { | ||||
|         Sqlite3Statement insert_stmt(db.db, "INSERT INTO memes (path, source, description, miscinfo) VALUES (?, ?, ?, ?)"); | ||||
|         insert_stmt.bind_text(1, path.native(), SQLITE_STATIC); | ||||
|         insert_stmt.bind_text(2, parser.arguments[2], SQLITE_STATIC); | ||||
|         insert_stmt.bind_text(3, parser.arguments[3], SQLITE_STATIC); | ||||
|         insert_stmt.bind_text(4, parser.arguments[4], SQLITE_STATIC); | ||||
|         db.db.exec(insert_stmt); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,43 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include "blankie/cliparse.h" | ||||
| 
 | ||||
| using Parser = blankie::cliparse::Parser; | ||||
| 
 | ||||
| #define HELP_TEXT &R"EOF( | ||||
| Usage: %1$s [-C=<directory>] create | ||||
|   or   %1$s [-C=<directory>] search <search query> | ||||
|   or   %1$s [-C=<directory>] prune | ||||
| 
 | ||||
|   or   %1$s [-C=<directory>] {info|show} <file>... | ||||
|   or   %1$s [-C=<directory>] delete <file>... | ||||
|   or   %1$s [-C=<directory>] edit <file> | ||||
|   or   %1$s [-C=<directory>] get <file> | ||||
|   or   %1$s [-C=<directory>] set <file> <source> <description> <misc info> | ||||
| 
 | ||||
| -C=<directory> changes the current working directory to <directory> before a | ||||
| subcommand is executed. This causes the database to be looked up in | ||||
| <directory>, all file paths are relative to <directory> and database creation | ||||
| to be inside of <directory> | ||||
| 
 | ||||
| create creates a database in the current directory (or <directory>) | ||||
| 
 | ||||
| prune removes all metadata entries that correspond to non-existent files, then | ||||
| vaccums the database | ||||
| 
 | ||||
| delete only deletes the metadata for the files specified, not the actual files | ||||
| themselves | ||||
| 
 | ||||
| get outputs the file's path, source, description, and miscellaneous information | ||||
| delimited by a null-byte | ||||
| )EOF"[1] | ||||
| 
 | ||||
| void subcommand_create(const Parser& parser); | ||||
| void subcommand_search(const Parser& parser); | ||||
| void subcommand_prune(const Parser& parser); | ||||
| 
 | ||||
| void subcommand_info(const Parser& parser); | ||||
| void subcommand_delete(const Parser& parser); | ||||
| void subcommand_edit(const Parser& parser); | ||||
| void subcommand_get(const Parser& parser); | ||||
| void subcommand_set(const Parser& parser); | ||||
|  | @ -0,0 +1,103 @@ | |||
| #include <cstring> | ||||
| #include <unistd.h> | ||||
| 
 | ||||
| #include "database.h" | ||||
| #include "utils.h" | ||||
| 
 | ||||
| static std::filesystem::path normalize(std::filesystem::path path); | ||||
| static std::optional<std::filesystem::path> relative_path_from(const std::filesystem::path& from, const std::filesystem::path& path); | ||||
| 
 | ||||
| void output_meme(const char* path, const char* source, const char* description, const char* miscinfo, int fd, bool edit) { | ||||
|     auto output = [=](const char* header, const char* contents, bool end = false) { | ||||
|         write(fd, "# ", 2); | ||||
|         write(fd, header, strlen(header)); | ||||
|         write(fd, ":\n", 2); | ||||
|         write(fd, contents, strlen(contents)); | ||||
|         write(fd, "\n", 1); | ||||
|         if (!end) { | ||||
|             write(fd, edit ? "\n\0\n" : "\n", edit ? 3 : 1); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     output(edit ? "Path (immutable)" : "Path", path); | ||||
|     output("Source", source); | ||||
|     output("Description", description); | ||||
|     output("Miscellaneous information", miscinfo, true); | ||||
| } | ||||
| 
 | ||||
| std::vector<std::filesystem::path> get_paths_relative_to_database(const blankie::cliparse::Parser& parser, const Database& db, int* rc, bool needs_exist) { | ||||
|     std::vector<std::filesystem::path> paths; | ||||
| 
 | ||||
|     paths.reserve(parser.arguments.size() - 1); | ||||
|     for (size_t i = 1; i < parser.arguments.size(); i++) { | ||||
|         std::filesystem::path normalized_path = normalize(parser.arguments[i]); | ||||
|         std::optional<std::filesystem::path> relative_path = relative_path_from(db.path.parent_path(), normalized_path); | ||||
|         if (!relative_path) { | ||||
|             fprintf(stderr, "%s is not in %s\n", normalized_path.c_str(), db.path.parent_path().c_str()); | ||||
|             *rc = 1; | ||||
|         } else if (needs_exist && !std::filesystem::is_regular_file(normalized_path)) { | ||||
|             fprintf(stderr, "%s is not a file\n", normalized_path.c_str()); | ||||
|             *rc = 1; | ||||
|         } else { | ||||
|             paths.push_back(*relative_path); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return paths; | ||||
| } | ||||
| 
 | ||||
| std::filesystem::path get_path_relative_to_database(const char* path, const Database& db) { | ||||
|     std::filesystem::path normalized_path = normalize(path); | ||||
|     std::optional<std::filesystem::path> relative_path = relative_path_from(db.path.parent_path(), normalized_path); | ||||
| 
 | ||||
|     if (!relative_path) { | ||||
|         fprintf(stderr, "%s is not in %s\n", normalized_path.c_str(), db.path.parent_path().c_str()); | ||||
|         exit(1); | ||||
|     } | ||||
|     if (!std::filesystem::is_regular_file(normalized_path)) { | ||||
|         fprintf(stderr, "%s is not a file\n", normalized_path.c_str()); | ||||
|         exit(1); | ||||
|     } | ||||
|     return *relative_path; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| std::filesystem::path normalize(std::filesystem::path path) { | ||||
|     if (path.is_relative()) { | ||||
|         path = std::filesystem::current_path() / path; | ||||
|     } | ||||
| 
 | ||||
|     std::filesystem::path normalized = path.root_path(); | ||||
|     for (auto it = path.begin(); it != path.end(); it++) { | ||||
|         if (*it == ".") { | ||||
|             // do nothing
 | ||||
|         } else if (*it == "..") { | ||||
|             normalized = normalized.parent_path(); | ||||
|         } else { | ||||
|             normalized /= *it; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return normalized; | ||||
| } | ||||
| 
 | ||||
| std::optional<std::filesystem::path> relative_path_from(const std::filesystem::path& from, const std::filesystem::path& path) { | ||||
|     auto from_it = from.begin(); | ||||
|     auto path_it = path.begin(); | ||||
| 
 | ||||
|     for (; from_it != from.end() && path_it != path.end(); from_it++, path_it++) { | ||||
|         if (*from_it != *path_it) { | ||||
|             return std::nullopt; | ||||
|         } | ||||
|     } | ||||
|     if (from_it != from.end()) { | ||||
|         return std::nullopt; | ||||
|     } | ||||
| 
 | ||||
|     std::filesystem::path name; | ||||
|     for (; path_it != path.end(); path_it++) { | ||||
|         name /= *path_it; | ||||
|     } | ||||
|     return name; | ||||
| } | ||||
|  | @ -0,0 +1,11 @@ | |||
| #pragma once | ||||
| 
 | ||||
| #include <optional> | ||||
| #include <filesystem> | ||||
| 
 | ||||
| struct Database; // forward declaration from database.h
 | ||||
| #include "blankie/cliparse.h" | ||||
| 
 | ||||
| void output_meme(const char* path, const char* source, const char* description, const char* miscinfo, int fd = 1, bool edit = false); | ||||
| std::vector<std::filesystem::path> get_paths_relative_to_database(const blankie::cliparse::Parser& parser, const Database& db, int* rc, bool needs_exist = true); | ||||
| std::filesystem::path get_path_relative_to_database(const char* path, const Database& db); | ||||
		Loading…
	
		Reference in New Issue