#include #include #include #include #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; }