https://codeberg.org/interpeer/vessel.git
Raw File
Tip revision: 5c7a39214ce9b5396b7aafb9d576b159b57ef04a authored by Jens Finkhaeuser on 09 January 2024, 12:10:19 UTC
(void) and () have different meanings in C
Tip revision: 5c7a392
main.cpp
/*
 * This file is part of vessel.
 *
 * Author(s): Jens Finkhaeuser <jens@finkhaeuser.de>
 *
 * Copyright (c) 2023 Interpeer gUG (haftungsbeschränkt).
 *
 * SPDX-License-Identifier: GPL-3.0-only
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#include <iostream>
#include <fstream>

#include <clipp.h>

#include <vessel.h>

#include "options.h"
#include "context.h"
#include "errors.h"
#include "commands.h"

namespace vessel::cli {

inline clipp::parameter
filename(std::string const & label, std::filesystem::path & path, bool required = true)
{
  auto func = [&](char const * arg)
  {
    path = arg;
  };
  if (required) {
    return clipp::value(label).call(func);
  }
  return clipp::opt_value(label).call(func);
}

inline clipp::parameter
filenames(std::string const & label, std::vector<std::filesystem::path> & paths, bool required = true)
{
  auto func = [&](char const * arg)
  {
    paths.push_back(arg);
  };
  if (required) {
    return clipp::values(label).call(func);
  }
  return clipp::opt_values(label).call(func);
}


static constexpr char const * intro =
  "   This command line utility allows you to create, inspect and manipulate\n"
  "   vessel resources.\n\n"
  "   Vessel defines a generic container format for encapsulating arbitrary\n"
  "   data - you can use it to wrap entire files or as a basis for your own\n"
  "   storage format. Vessel provides authenticated and encrypted storage,\n"
  "   chunking of storage files, multiplexing separate data streams, etc.\n\n"
  "   The effect is that vessel resources can be securely distributed via\n"
  "   any transport medium, from sneakernet to the internet.\n"
;


VESSEL_PRIVATE
void
parse_cli(options & opts, int argc, char **argv)
{
  using namespace clipp;

  auto resource = filename("resource", opts.resource_file)
        .doc("The resource to create, inspect or manipulate.");

  group commands;
  commands.scoped(false).exclusive(true);

  commands.push_back(
      "Import command" %

      (command("import")
        .set(opts.command, CMD_IMPORT)
        .doc("Imports an arbitrary file into a vessel resource. If no input "
          "file is provided, data to import is read from standard input.")

        , resource

        , filenames("input file", opts.input_files, false)
          .doc("File(s) to import.")

        , (
          (option("-a", "--append")
          .set(opts.import_append))
          .doc("If set, appends the file to an existing resource, "
            "otherwise the command overwrites the existing resource. "
            "If no resource exists yet, the option makes no difference.")

          , (option("-m", "--mime-type")
              & value("type", opts.mime_type))
            .doc("If the input file MIME type cannot be detected, set it "
              "with this option. Defaults to '" + opts.mime_type + "'. "
              "Note that when importing multiple files of different types, "
              "this option forces all files to be of the same type. It's "
              "better to use --append in that case and add files one by one.")

          , (option("-s", "--extent-size-multiplier")
              & number("size", opts.extent_size_multiplier))
            .doc("Extents are 4096 bytes in size times this multiplier; "
              "the default multiplier is: "
              + std::to_string(opts.extent_size_multiplier))

          , (option("-t", "--topic")
              & number("topic", opts.topic))
            .doc("If specified, the topic to give to sections containing "
                "the imported file's data. If none is given or a topic below "
                "1024 is provided, a random topic above 1024 is chosen. "
                "If multiple input files are provided on the command line, "
                "each is placed into a separate topic with the topic number "
                "incremented from the one set here (or randomly chosen).")
        )
      )
  );

  commands.push_back(
      "Export command" %

      (command("export")
        .set(opts.command, CMD_EXPORT)
        .doc("Export a file (BLOB sections) from a resource and/or topic.\n"
          "\n"
          "Resources can contain many topics, and each of them can contain "
          "multiple files. If neither --topic nor --all are given, the command "
          "exports the first file it detects and exits.\n"
          "\n"
          "If --topic is given, all files from that topic are exported. If "
          "--all is provided, all files from all topics are exported."
        )

        , resource

        , (
          option("-a", "--all")
          .set(opts.export_all)
          .doc("If provided, export all files from the topic(s), otherwise stop "
            "after the first file.")

          , (option("-t", "--topic")
              & number("topic", opts.topic))
            .doc("If specified, export only the file(s) from this topic, otherwise "
              "process all topics.")

          , (option("-o", "--output-name")
              & value("filename", opts.export_filename))
            .doc("Use this output file name instead of the name provided in the "
              "content type. If '-' is given, write to stdout.")
        )
      )
  );

  commands.push_back(
      "Info command" %

      (command("info")
        .set(opts.command, CMD_INFO)
        .doc("Displays information about a vessel resource.")

        , resource

        , (
          option("-f", "--full-ids")
            .set(opts.full_ids)
            .doc("If set, does not truncate extent and author identifiers.")

          , option("-s", "--sections")
              .set(opts.sections)
              .doc("If set, also display information about individual sections "
                "in the resource.")
        )
      )
  );

  commands.push_back(
      "List command" %

      (command("ls")
        .set(opts.command, CMD_LS)
        .doc("Scans a resource for embedded files, and displays information about them.")

        , resource
      )
  );


  bool help = false;
  bool version = false;

  auto global_opts = "Global Options" % (
      option("-?", "--help")
        .set(help)
        .doc("Display this help.")

      , option("-v", "--version")
          .set(version)
          .doc("Display version information and exit.")
  );

  auto shared_opts = "Shared Options" % (
      (option("--algorithms")
          & value("specs", opts.algorithms))
        .doc("Specify the combination of (cryptograhic) algorithms to use for"
          " this resource. Defaults to '" + opts.algorithms + "'.")

      , (option("-k", "--author-key")
            & filename("file", opts.author_filename))
          .doc("A file containing an authoring key pair such as e.g. generated "
            "by OpenSSL, etc.")

      , option("--verbose")
         .set(opts.verbose)
         .doc("Verbose output; the interpration of this depends on the command.")
  );
  // TODO
  // "used by many commands"
  // - encrypt or not


  auto cli = (global_opts, shared_opts, commands);

  auto result = parse(argc, argv, cli);

#if 0
    // Parameter debugging
    auto doc_label = [](parameter const & p) {
        if (!p.flags().empty()) {
          return p.flags().front();
        }
        if (!p.label().empty()) {
          return p.label();
        }
        return doc_string{"<?>"};
    };

    for (auto const & m : result) {
      std::cerr << "#" << m.index() << " " << m.arg() << " -> ";
      auto p = m.param();
      if (p) {
        std::cerr << doc_label(*p) << " \t";
        if (m.repeat() > 0) {
          std::cerr << (m.bad_repeat() ? "[bad repeat " : "[repeat ")
                    << m.repeat() << "]";
        }
        if (m.blocked()) {
          std::cerr << " [blocked]";
        }
        if (m.conflict()) {
          std::cerr << " [conflict]";
        }
        std::cerr << std::endl;
      }
      else {
        std::cerr << " [unmapped]";
      }
    }
//cout << "missing parameters:\n";
//for(const auto& m : result.missing()) {
//    auto p = m.param();
//    if(p) {
//        os << doc_label(*p) << " \t";
//        os << " [missing after " << m.after_index() << "]\n";
//    }
//}
#endif

  // Help
  if (!result || help) {
    auto fmt = doc_formatting{}
        .first_column(3)
        .last_column(79);
    exit_log(VEXIT_HELP)
      << make_man_page(cli, argv[0], fmt)
        .prepend_section("INTRODUCTION", intro)
        .prepend_section("VERSION INFORMATION", vessel_copyright_string())
      ;
  }

  // FIXME version alone does not work at the moment

  // Version
  if (version) {
    exit_log(VEXIT_OK, std::cout) << vessel_copyright_string();
  }
}



VESSEL_PRIVATE
void
initialize_context(context & ctx, options const & opts)
{
  // Options
  ctx.opts = opts;

  // Algorithms
  // TODO if none was given, create the algorithm context from one of the
  //      plugins - or always do that, even if one was given?
  ctx.algos = vessel_algorithm_choices_create(opts.algorithms.c_str());
  if (!ctx.algos.version_tag) {
    exit_log(VEXIT_ALGOS) << "Bad algorithm choices: " << opts.algorithms;
  }

  auto err = vessel_algorithm_context_create_from_choices(&ctx.algo_ctx,
      &(ctx.algos), 1);
  if (VESSEL_ERR_SUCCESS != err) {
    exit_log(VEXIT_ALGOS, err) << "Error creating algorithm context.";
  }

  // Context
  err = vessel_context_create(&ctx.ctx, ctx.algo_ctx);
  if (VESSEL_ERR_SUCCESS != err) {
    exit_log(VEXIT_INIT, err) << "Error creating context.";
  }

  // Author key
  if (!opts.author_filename.empty()) {
    std::ifstream keyfile;
    keyfile.open(opts.author_filename);
    if (!keyfile.is_open()) {
      exit_log(VEXIT_AUTHOR) << "Could not open author key file: " << opts.author_filename;
    }

    std::vector<char> keybuf;
    keybuf.resize(4096); // Assume this is enough.
    keyfile.read(&keybuf[0], keybuf.size());
    keybuf.resize(keyfile.gcount());
    if (!keybuf.size()) {
      exit_log(VEXIT_AUTHOR) << "Author key file was empty!";
    }

    err = vessel_author_from_buffer(&ctx.author, &ctx.algos,
        keybuf.data(), keybuf.size());
    if (VESSEL_ERR_SUCCESS != err) {
      exit_log(VEXIT_AUTHOR, err) << "Could not create author from key file.";
    }

    // Author ID
    err = vessel_author_id_from_author(&ctx.author_id, &ctx.algos, ctx.author);
    if (VESSEL_ERR_SUCCESS != err) {
      exit_log(VEXIT_AUTHOR, err) << "Could not create author ID without author.";
    }
  }
}

} // namsepace vessel::cli


int main(int argc, char **argv)
{
  using namespace vessel::cli;

  // Parse CLI options
  options opts;
  parse_cli(opts, argc, argv);

  // Try to initialize context from parsed options
  context ctx;
  initialize_context(ctx, opts);

  // Find (and run) command
  auto iter = command_registry.find(opts.command);
  if (iter == command_registry.end() || !iter->second) {
    exit_log(VEXIT_NO_CMD) << "Command not implemented!";
  }

  return iter->second(ctx);
}
back to top