mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 10:00:05 +02:00
128 lines
6.9 KiB
C++
128 lines
6.9 KiB
C++
#include "cli.hpp"
|
|
#include <array>
|
|
#include <filesystem>
|
|
#include <string_view>
|
|
|
|
namespace frameyap {
|
|
namespace {
|
|
using Mode = CliMode;
|
|
constexpr unsigned bit(Mode mode) { return 1u << static_cast<unsigned>(mode); }
|
|
constexpr unsigned run = bit(Mode::Run), input = bit(Mode::CheckInput);
|
|
constexpr unsigned visual = bit(Mode::CheckOverlay) | bit(Mode::CheckControls);
|
|
constexpr unsigned models = bit(Mode::ListModels) | bit(Mode::CheckModel);
|
|
struct ModeSpec { std::string_view name; Mode mode; bool positional; };
|
|
constexpr std::array modes{
|
|
ModeSpec{"--help", Mode::Help, false}, ModeSpec{"--version", Mode::Version, false},
|
|
ModeSpec{"--run", Mode::Run, false}, ModeSpec{"--register", Mode::Register, true},
|
|
ModeSpec{"--unregister", Mode::Unregister, true},
|
|
ModeSpec{"--check-input", Mode::CheckInput, false},
|
|
ModeSpec{"--check-overlay", Mode::CheckOverlay, false},
|
|
ModeSpec{"--check-controls", Mode::CheckControls, false},
|
|
ModeSpec{"--list-models", Mode::ListModels, false},
|
|
ModeSpec{"--check-model", Mode::CheckModel, true},
|
|
};
|
|
struct OptionSpec { std::string_view name; unsigned allowed; std::string CliOptions::* field; bool flag; };
|
|
constexpr std::array options{
|
|
OptionSpec{"--assets", run | visual, &CliOptions::assets, false},
|
|
OptionSpec{"--font", run | visual, &CliOptions::font, false},
|
|
OptionSpec{"--socket", run | input | bit(Mode::CheckControls), &CliOptions::socket, false},
|
|
OptionSpec{"--mount", run | visual, &CliOptions::mount, false},
|
|
OptionSpec{"--worker", run, &CliOptions::worker, false},
|
|
OptionSpec{"--python", run, &CliOptions::python, false},
|
|
OptionSpec{"--model", run, &CliOptions::model, false},
|
|
OptionSpec{"--threads", run, &CliOptions::threads, false},
|
|
OptionSpec{"--model-dir", models, &CliOptions::model_dir, false},
|
|
OptionSpec{"--backend", run, &CliOptions::backend, false},
|
|
OptionSpec{"--model-store", run, &CliOptions::model_store, false},
|
|
OptionSpec{"--manifest-dir", models | run, &CliOptions::manifest_dir, false},
|
|
OptionSpec{"--head", run | visual, nullptr, true},
|
|
OptionSpec{"--autostart", bit(Mode::Register), nullptr, true},
|
|
OptionSpec{"--json", models, nullptr, true},
|
|
};
|
|
// Matches python/frameyap/model_files.py's manifest ID grammar. Reject invalid
|
|
// IDs before runtime setup; no basename or option-looking ID can be selected.
|
|
bool valid_backend(std::string_view id) {
|
|
if (id.empty() || id.size() > 48 || id.front() < 'a' || id.front() > 'z') return false;
|
|
for (char c : id) if (!((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-')) return false;
|
|
return true;
|
|
}
|
|
bool safe_absolute_path(const std::string& value) {
|
|
const std::filesystem::path path(value);
|
|
if (!path.is_absolute()) return false;
|
|
for (const auto& part : path) if (part == "." || part == "..") return false;
|
|
for (unsigned char c : value) if (c < 32 || c == 127) return false;
|
|
return true;
|
|
}
|
|
} // namespace
|
|
std::filesystem::path asset_root(const std::string& assets) {
|
|
if (assets.empty()) return {};
|
|
auto path = std::filesystem::absolute(std::filesystem::path(assets)).lexically_normal();
|
|
// path("assets/") has an empty final component on some standard libraries.
|
|
while (path.filename().empty() && path.has_parent_path() && path != path.root_path())
|
|
path = path.parent_path();
|
|
return path.parent_path();
|
|
}
|
|
bool is_managed_worker(const std::string& assets, const std::string& worker) {
|
|
if (assets.empty() || worker.empty()) return false;
|
|
const auto expected = (asset_root(assets) / "python/frameyap/worker.py").lexically_normal();
|
|
return std::filesystem::absolute(std::filesystem::path(worker)).lexically_normal() == expected;
|
|
}
|
|
CliOptions parse_cli(int argc, const char* const argv[]) {
|
|
CliOptions result;
|
|
if (argc <= 1) return result;
|
|
const std::string_view first(argv[1]);
|
|
const ModeSpec* spec = nullptr;
|
|
for (const auto& candidate : modes) if (first == candidate.name) { spec = &candidate; break; }
|
|
if (!spec) throw CliError("Unsupported arguments. Use --help.");
|
|
result.mode = spec->mode;
|
|
int i = 2;
|
|
if (spec->positional) {
|
|
if (i == argc || !argv[i][0] || argv[i][0] == '-')
|
|
throw CliError("Missing value for " + std::string(first));
|
|
if (result.mode == Mode::CheckModel) result.model_id = argv[i++];
|
|
else result.manifest = argv[i++];
|
|
}
|
|
for (; i < argc; ++i) {
|
|
std::string_view arg(argv[i]);
|
|
const OptionSpec* option = nullptr;
|
|
for (const auto& candidate : options) if (arg == candidate.name) { option = &candidate; break; }
|
|
if (!option || !(option->allowed & bit(result.mode)))
|
|
throw CliError("Unsupported arguments: " + std::string(arg));
|
|
if (option->flag) {
|
|
bool* target = arg == "--head" ? &result.head :
|
|
arg == "--json" ? &result.json : &result.autostart;
|
|
if (*target) throw CliError("Duplicate option: " + std::string(arg));
|
|
*target = true;
|
|
} else {
|
|
if (i + 1 == argc || !argv[i + 1][0] || argv[i + 1][0] == '-')
|
|
throw CliError("Missing value for " + std::string(arg));
|
|
auto& value = result.*(option->field);
|
|
if (!value.empty()) throw CliError("Duplicate option: " + std::string(arg));
|
|
value = argv[++i];
|
|
}
|
|
}
|
|
if (result.head && !result.mount.empty()) throw CliError("--head and --mount cannot be combined");
|
|
if (result.mode == Mode::Run && result.threads != "" && result.threads != "2" && result.threads != "4")
|
|
throw CliError("--threads must be 2 or 4");
|
|
if (result.mode == Mode::CheckModel && result.model_dir.empty())
|
|
throw CliError("--check-model requires --model-dir");
|
|
if (result.mode == Mode::CheckModel && !valid_backend(result.model_id))
|
|
throw CliError("--check-model requires a manifest ID ([a-z][a-z0-9_-]{0,47})");
|
|
if (result.mode == Mode::Run) {
|
|
if (!result.backend.empty() && !valid_backend(result.backend))
|
|
throw CliError("--backend must be a manifest ID ([a-z][a-z0-9_-]{0,47})");
|
|
if (!result.manifest_dir.empty() && !safe_absolute_path(result.manifest_dir))
|
|
throw CliError("--manifest-dir must be an absolute local directory without dot segments or controls");
|
|
if (!result.model_store.empty() && !safe_absolute_path(result.model_store))
|
|
throw CliError("--model-store must be an absolute local directory without dot segments or controls");
|
|
// Only the worker at the app root inferred from --assets belongs to
|
|
// the managed dispatcher. A namesake script elsewhere is custom.
|
|
if (!result.worker.empty() && !is_managed_worker(result.assets, result.worker)) {
|
|
if (!result.backend.empty()) throw CliError("--backend cannot be combined with a custom --worker");
|
|
if (result.model.empty()) throw CliError("--model required with a custom --worker");
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
} // namespace frameyap
|