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