Signet Forge 0.1.0
C++20 Parquet library with AI-native extensions
DEMO
Loading...
Searching...
No Matches
error.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright 2026 Johnson Ogundeji
3#pragma once
4
5#include <algorithm>
6#include <cassert>
7#include <stdexcept>
8#include <cctype>
9#include <chrono>
10#include <cstddef>
11#include <cstdint>
12#include <cstdio>
13#include <cstdlib>
14#include <cstring>
15#include <climits>
16#include <filesystem>
17#include <fstream>
18#include <limits>
19#include <mutex>
20#include <sstream>
21#include <string>
22#include <type_traits>
23#include <unordered_map>
24#include <utility>
25#include <unordered_set>
26#include <variant>
27#include <vector>
28
29#ifndef _WIN32
30#include <fcntl.h>
31#include <pwd.h>
32#include <sys/stat.h>
33#include <unistd.h>
34#endif
35
36namespace signet::forge {
37
38// ---------------------------------------------------------------------------
39// Error type — all signet operations return expected<T, Error>
40// ---------------------------------------------------------------------------
41
87
101struct Error {
105 std::string message;
106
112 Error(ErrorCode c, std::string msg) : code(c), message(std::move(msg)) {}
113
115 [[nodiscard]] bool ok() const { return code == ErrorCode::OK; }
118 [[nodiscard]] explicit operator bool() const { return !ok(); }
119};
120
121// ---------------------------------------------------------------------------
122// expected<T, Error> — lightweight result type
123// ---------------------------------------------------------------------------
124
144template <typename T>
145class expected {
146public:
149 expected(const T& val) requires std::is_copy_constructible_v<T>
150 : storage_(val) {}
153 expected(T&& val) : storage_(std::move(val)) {}
154
157 expected(const Error& err) : storage_(err) {}
160 expected(Error&& err) : storage_(std::move(err)) {}
161
165 expected(ErrorCode code, std::string msg)
166 : storage_(Error{code, std::move(msg)}) {}
167
169 [[nodiscard]] bool has_value() const { return std::holds_alternative<T>(storage_); }
172 [[nodiscard]] explicit operator bool() const { return has_value(); }
173
177 [[nodiscard]] const T& value() const& {
178 if (!has_value()) throw std::logic_error("expected::value() called on error");
179 return std::get<T>(storage_);
180 }
184 [[nodiscard]] T& value() & {
185 if (!has_value()) throw std::logic_error("expected::value() called on error");
186 return std::get<T>(storage_);
187 }
191 [[nodiscard]] T&& value() && {
192 if (!has_value()) throw std::logic_error("expected::value() called on error");
193 return std::get<T>(std::move(storage_));
194 }
195
199 [[nodiscard]] const Error& error() const {
200 if (has_value()) throw std::logic_error("expected::error() called on value");
201 return std::get<Error>(storage_);
202 }
203
206 [[nodiscard]] const T& operator*() const& { return value(); }
209 [[nodiscard]] T& operator*() & { return value(); }
212 [[nodiscard]] T&& operator*() && { return std::move(*this).value(); }
215 [[nodiscard]] const T* operator->() const { return &value(); }
218 [[nodiscard]] T* operator->() { return &value(); }
219
220private:
221 std::variant<T, Error> storage_;
222};
223
238template <>
239class expected<void> {
240public:
242 expected() : err_() {}
245 expected(const Error& err) : err_(err) {}
248 expected(Error&& err) : err_(std::move(err)) {}
252 expected(ErrorCode code, std::string msg) : err_(Error{code, std::move(msg)}) {}
253
255 [[nodiscard]] bool has_value() const { return err_.ok(); }
258 [[nodiscard]] explicit operator bool() const { return has_value(); }
261 [[nodiscard]] const Error& error() const { return err_; }
262
263private:
264 Error err_;
265};
266
267} // namespace signet::forge
268
269namespace signet::forge {
270
276namespace commercial {
277
279inline constexpr const char* kLicenseEnvVar = "SIGNET_COMMERCIAL_LICENSE_KEY";
281inline constexpr const char* kLicenseTierEnvVar = "SIGNET_COMMERCIAL_LICENSE_TIER";
283inline constexpr const char* kUsageFileEnvVar = "SIGNET_COMMERCIAL_USAGE_FILE";
285inline constexpr const char* kRuntimeUserEnvVar = "SIGNET_COMMERCIAL_RUNTIME_USER";
287inline constexpr const char* kRuntimeNodeEnvVar = "SIGNET_COMMERCIAL_RUNTIME_NODE";
289inline constexpr uint64_t kUsagePersistIntervalRows = 10'000;
290
293#if defined(SIGNET_EVAL_MAX_ROWS_MONTH_U64)
294inline constexpr uint64_t kDefaultEvalMaxRowsMonth =
295 static_cast<uint64_t>(SIGNET_EVAL_MAX_ROWS_MONTH_U64);
296#else
297inline constexpr uint64_t kDefaultEvalMaxRowsMonth = 50'000'000ull;
298#endif
299
302#if defined(SIGNET_EVAL_MAX_USERS_U32)
303inline constexpr uint32_t kDefaultEvalMaxUsers =
304 static_cast<uint32_t>(SIGNET_EVAL_MAX_USERS_U32);
305#else
306inline constexpr uint32_t kDefaultEvalMaxUsers = 3u;
307#endif
308
311#if defined(SIGNET_EVAL_MAX_NODES_U32)
312inline constexpr uint32_t kDefaultEvalMaxNodes =
313 static_cast<uint32_t>(SIGNET_EVAL_MAX_NODES_U32);
314#else
315inline constexpr uint32_t kDefaultEvalMaxNodes = 1u;
316#endif
317
320#if defined(SIGNET_EVAL_MAX_DAYS_U32)
321inline constexpr uint32_t kDefaultEvalMaxDays =
322 static_cast<uint32_t>(SIGNET_EVAL_MAX_DAYS_U32);
323#else
324inline constexpr uint32_t kDefaultEvalMaxDays = 30u;
325#endif
326
329#if defined(SIGNET_EVAL_WARN_PCT_1_U32)
330inline constexpr uint32_t kDefaultEvalWarnPct1 =
331 static_cast<uint32_t>(SIGNET_EVAL_WARN_PCT_1_U32);
332#else
333inline constexpr uint32_t kDefaultEvalWarnPct1 = 80u;
334#endif
335
338#if defined(SIGNET_EVAL_WARN_PCT_2_U32)
339inline constexpr uint32_t kDefaultEvalWarnPct2 =
340 static_cast<uint32_t>(SIGNET_EVAL_WARN_PCT_2_U32);
341#else
342inline constexpr uint32_t kDefaultEvalWarnPct2 = 90u;
343#endif
344
352struct LicensePolicy {
354 bool evaluation_mode{false};
356 uint64_t max_rows_month{0};
358 uint32_t max_users{0};
360 uint32_t max_nodes{0};
362 int64_t explicit_expiry_day_utc{0};
364 uint32_t max_eval_days{0};
366 uint32_t warn_pct_1{kDefaultEvalWarnPct1};
368 uint32_t warn_pct_2{kDefaultEvalWarnPct2};
369};
370
379struct UsageState {
381 bool initialized{false};
383 int month_tag{0};
385 uint64_t rows_this_month{0};
387 uint64_t last_persisted_rows_this_month{0};
389 int64_t eval_start_day_utc{0};
391 bool warn_pct_1_emitted{false};
393 bool warn_pct_2_emitted{false};
395 std::unordered_set<std::string> users;
397 std::unordered_set<std::string> nodes;
398};
399
406[[nodiscard]] inline uint64_t fnv1a64(const char* data, size_t size) noexcept {
407 constexpr uint64_t kOffset = 14695981039346656037ull;
408 constexpr uint64_t kPrime = 1099511628211ull;
409
410 uint64_t hash = kOffset;
411 for (size_t i = 0; i < size; ++i) {
412 hash ^= static_cast<uint8_t>(data[i]);
413 hash *= kPrime;
414 }
415 return hash;
416}
417
419[[nodiscard]] inline std::string trim_copy(const std::string& in) {
420 size_t start = 0;
421 while (start < in.size() && std::isspace(static_cast<unsigned char>(in[start]))) {
422 ++start;
423 }
424
425 size_t end = in.size();
426 while (end > start && std::isspace(static_cast<unsigned char>(in[end - 1]))) {
427 --end;
428 }
429
430 return in.substr(start, end - start);
431}
432
434[[nodiscard]] inline std::string to_lower_ascii(std::string value) {
435 for (char& ch : value) {
436 ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
437 }
438 return value;
439}
440
442[[nodiscard]] inline std::string sanitize_identity(std::string value,
443 const char* fallback) {
444 value = trim_copy(value);
445 if (value.empty()) {
446 return fallback;
447 }
448
449 if (value.size() > 128) {
450 value.resize(128);
451 }
452
453 for (char& c : value) {
454 if (std::isalnum(static_cast<unsigned char>(c)) ||
455 c == '-' || c == '_' || c == '.' || c == '@') {
456 continue;
457 }
458 c = '_';
459 }
460 return value;
461}
462
464[[nodiscard]] inline std::unordered_map<std::string, std::string>
465parse_claims(const std::string& text) {
466 std::unordered_map<std::string, std::string> out;
467 std::stringstream ss(text);
468 std::string token;
469
470 while (std::getline(ss, token, ';')) {
471 auto pos = token.find('=');
472 if (pos == std::string::npos || pos == 0 || pos + 1 >= token.size()) {
473 continue;
474 }
475 std::string key = to_lower_ascii(trim_copy(token.substr(0, pos)));
476 std::string val = trim_copy(token.substr(pos + 1));
477 if (!key.empty() && !val.empty()) {
478 out[key] = val;
479 }
480 }
481
482 return out;
483}
484
486[[nodiscard]] inline bool parse_u64(const std::string& text, uint64_t& out) {
487 if (text.empty()) return false;
488
489 uint64_t value = 0;
490 for (char ch : text) {
491 if (!std::isdigit(static_cast<unsigned char>(ch))) return false;
492 const uint64_t digit = static_cast<uint64_t>(ch - '0');
493 if (value > ((std::numeric_limits<uint64_t>::max)() - digit) / 10ull) {
494 return false;
495 }
496 value = (value * 10ull) + digit;
497 }
498
499 out = value;
500 return true;
501}
502
504[[nodiscard]] inline bool parse_u32(const std::string& text, uint32_t& out) {
505 uint64_t value = 0;
506 if (!parse_u64(text, value) || value > (std::numeric_limits<uint32_t>::max)()) {
507 return false;
508 }
509 out = static_cast<uint32_t>(value);
510 return true;
511}
512
514[[nodiscard]] inline constexpr int64_t days_from_civil(int y, unsigned m,
515 unsigned d) noexcept {
516 y -= m <= 2;
517 const int era = (y >= 0 ? y : y - 399) / 400;
518 const unsigned yoe = static_cast<unsigned>(y - era * 400);
519 const unsigned mp = (m > 2u) ? (m - 3u) : (m + 9u);
520 const unsigned doy = (153u * mp + 2u) / 5u + d - 1u;
521 const unsigned doe = yoe * 365u + yoe / 4u - yoe / 100u + doy;
522 return static_cast<int64_t>(era) * 146097 + static_cast<int64_t>(doe) - 719468;
523}
524
526[[nodiscard]] inline bool is_leap_year(int y) noexcept {
527 return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
528}
529
531[[nodiscard]] inline bool parse_iso_date_to_epoch_day(const std::string& iso,
532 int64_t& out_day) {
533 if (iso.size() != 10 || iso[4] != '-' || iso[7] != '-') return false;
534
535 auto parse_two = [&](size_t idx, int& out) -> bool {
536 if (!std::isdigit(static_cast<unsigned char>(iso[idx])) ||
537 !std::isdigit(static_cast<unsigned char>(iso[idx + 1]))) {
538 return false;
539 }
540 out = (iso[idx] - '0') * 10 + (iso[idx + 1] - '0');
541 return true;
542 };
543
544 int year = 0;
545 for (size_t i = 0; i < 4; ++i) {
546 if (!std::isdigit(static_cast<unsigned char>(iso[i]))) return false;
547 year = year * 10 + (iso[i] - '0');
548 }
549
550 int month = 0;
551 int day = 0;
552 if (!parse_two(5, month) || !parse_two(8, day)) return false;
553 if (month < 1 || month > 12) return false;
554
555 static constexpr int kMonthDays[] = {31,28,31,30,31,30,31,31,30,31,30,31};
556 int max_day = kMonthDays[month - 1];
557 if (month == 2 && is_leap_year(year)) max_day = 29;
558 if (day < 1 || day > max_day) return false;
559
560 out_day = days_from_civil(year, static_cast<unsigned>(month), static_cast<unsigned>(day));
561 return true;
562}
563
565[[nodiscard]] inline int64_t current_epoch_day_utc() {
566 using namespace std::chrono;
567 const auto now_days = floor<days>(system_clock::now());
568 return now_days.time_since_epoch().count();
569}
570
572[[nodiscard]] inline int current_month_tag_utc() {
573 using namespace std::chrono;
574 const auto now_days = floor<days>(system_clock::now());
575 const year_month_day ymd{now_days};
576 return static_cast<int>(static_cast<int>(ymd.year()) * 100 +
577 static_cast<unsigned>(ymd.month()));
578}
579
584[[nodiscard]] inline std::string default_usage_state_path() {
585 // Prefer XDG_STATE_HOME (e.g. ~/.local/state)
586 const char* xdg = std::getenv("XDG_STATE_HOME");
587 if (xdg && xdg[0]) {
588 return std::string(xdg) + "/signet-forge/usage_state";
589 }
590 // Fall back to HOME-based path
591 const char* home = std::getenv("HOME");
592#ifdef _WIN32
593 if (!home || !home[0]) home = std::getenv("USERPROFILE");
594#endif
595 if (home && home[0]) {
596 return std::string(home) + "/.local/state/signet-forge/usage_state";
597 }
598 // Last resort: /tmp (less secure but functional)
599#if defined(SIGNET_COMMERCIAL_LICENSE_HASH_U64)
600 char buf[96];
601 std::snprintf(buf, sizeof(buf),
602 "/tmp/signet_commercial_usage_%016llx.state",
603 static_cast<unsigned long long>(
604 static_cast<uint64_t>(SIGNET_COMMERCIAL_LICENSE_HASH_U64)));
605 return std::string(buf);
606#else
607 return "/tmp/signet_commercial_usage.state";
608#endif
609}
610
615[[nodiscard]] inline std::string usage_state_path() {
616 const char* env = std::getenv(kUsageFileEnvVar);
617 if (env != nullptr && env[0] != '\0') {
618 std::string raw(env);
619
620 // CWE-22: Reject path traversal sequences.
621 if (raw.find("..") != std::string::npos)
622 return default_usage_state_path();
623
624 // CWE-22: Reject embedded null bytes (truncation attack).
625 if (raw.find('\0') != std::string::npos)
626 return default_usage_state_path();
627
628#ifndef _WIN32
629 // CWE-22: Require absolute path — relative paths are ambiguous and
630 // depend on the process cwd, which the env setter may not control.
631 if (raw.empty() || raw[0] != '/')
632 return default_usage_state_path();
633
634 // CWE-426: Canonicalize parent directory via realpath() to resolve
635 // symlinks. The parent MUST exist and be a real directory.
636 auto fs_path = std::filesystem::path(raw);
637 auto parent = fs_path.parent_path();
638 if (parent.empty())
639 return default_usage_state_path();
640
641 char resolved[PATH_MAX] = {};
642 if (!::realpath(parent.string().c_str(), resolved))
643 return default_usage_state_path(); // parent doesn't exist
644
645 // Verify the resolved parent is actually a directory (not a file or device).
646 std::error_code ec;
647 if (!std::filesystem::is_directory(resolved, ec))
648 return default_usage_state_path();
649
650 // Build sanitised path from canonicalized parent + original filename.
651 std::string sanitised = std::string(resolved) + "/" +
652 fs_path.filename().string();
653
654 // Final guard: reject if canonicalization re-introduced ".."
655 if (sanitised.find("..") != std::string::npos)
656 return default_usage_state_path();
657
658 return sanitised;
659#else
660 // Windows: basic validation only (no realpath).
661 return raw;
662#endif
663 }
664 return default_usage_state_path();
665}
666
668[[nodiscard]] inline std::string detect_runtime_user() {
669#if defined(SIGNET_ALLOW_RUNTIME_IDENTITY_ENV) && SIGNET_ALLOW_RUNTIME_IDENTITY_ENV
670 const char* env_user = std::getenv(kRuntimeUserEnvVar);
671 if (env_user != nullptr && env_user[0] != '\0') {
672 return sanitize_identity(std::string(env_user), "unknown-user");
673 }
674#endif
675
676#ifndef _WIN32
677 struct passwd pwd_buf {};
678 struct passwd* pwd = nullptr;
679 char storage[4096] = {};
680 if (::getpwuid_r(::geteuid(), &pwd_buf, storage, sizeof(storage), &pwd) == 0 &&
681 pwd != nullptr && pwd->pw_name != nullptr && pwd->pw_name[0] != '\0') {
682 return sanitize_identity(std::string(pwd->pw_name), "unknown-user");
683 }
684
685 const char* user = std::getenv("USER");
686 if (user == nullptr || user[0] == '\0') {
687 user = std::getenv("LOGNAME");
688 }
689#else
690 const char* user = std::getenv("USERNAME");
691#endif
692 return sanitize_identity(user ? std::string(user) : std::string(), "unknown-user");
693}
694
696[[nodiscard]] inline std::string detect_runtime_node() {
697#if defined(SIGNET_ALLOW_RUNTIME_IDENTITY_ENV) && SIGNET_ALLOW_RUNTIME_IDENTITY_ENV
698 const char* env_node = std::getenv(kRuntimeNodeEnvVar);
699 if (env_node != nullptr && env_node[0] != '\0') {
700 return sanitize_identity(std::string(env_node), "unknown-node");
701 }
702#endif
703
704#ifndef _WIN32
705 char host[256] = {};
706 if (::gethostname(host, sizeof(host) - 1) == 0) {
707 host[sizeof(host) - 1] = '\0';
708 return sanitize_identity(std::string(host), "unknown-node");
709 }
710 const char* node = std::getenv("HOSTNAME");
711#else
712 const char* node = std::getenv("COMPUTERNAME");
713#endif
714 return sanitize_identity(node ? std::string(node) : std::string(), "unknown-node");
715}
716
718[[nodiscard]] inline std::vector<std::string> split_csv(const std::string& text) {
719 std::vector<std::string> out;
720 std::stringstream ss(text);
721 std::string token;
722
723 while (std::getline(ss, token, ',')) {
724 token = sanitize_identity(token, "");
725 if (!token.empty()) {
726 out.push_back(token);
727 }
728 }
729
730 return out;
731}
732
734[[nodiscard]] inline std::string join_set_csv(const std::unordered_set<std::string>& values) {
735 std::vector<std::string> ordered;
736 ordered.reserve(values.size());
737 for (const auto& v : values) {
738 ordered.push_back(v);
739 }
740 std::sort(ordered.begin(), ordered.end());
741
742 std::string out;
743 for (size_t i = 0; i < ordered.size(); ++i) {
744 if (i != 0) out.push_back(',');
745 out += ordered[i];
746 }
747 return out;
748}
749
751inline void load_usage_state_from_file(const std::string& path, UsageState& st) {
752 std::ifstream in(path);
753 if (!in.good()) {
754 return;
755 }
756
757 std::string line;
758 while (std::getline(in, line)) {
759 auto eq = line.find('=');
760 if (eq == std::string::npos) continue;
761
762 const std::string key = trim_copy(line.substr(0, eq));
763 const std::string val = trim_copy(line.substr(eq + 1));
764
765 uint64_t u64 = 0;
766 if (key == "month_tag" && parse_u64(val, u64)) {
767 st.month_tag = static_cast<int>(u64);
768 } else if (key == "rows_this_month" && parse_u64(val, u64)) {
769 st.rows_this_month = u64;
770 } else if (key == "eval_start_day_utc" && parse_u64(val, u64)) {
771 st.eval_start_day_utc = static_cast<int64_t>(u64);
772 } else if (key == "warn_pct_1_emitted") {
773 st.warn_pct_1_emitted = (val == "1");
774 } else if (key == "warn_pct_2_emitted") {
775 st.warn_pct_2_emitted = (val == "1");
776 } else if (key == "users") {
777 for (auto& token : split_csv(val)) {
778 st.users.insert(token);
779 }
780 } else if (key == "nodes") {
781 for (auto& token : split_csv(val)) {
782 st.nodes.insert(token);
783 }
784 }
785 }
786
787 st.last_persisted_rows_this_month = st.rows_this_month;
788}
789
793[[nodiscard]] inline bool persist_usage_state_to_file(const std::string& path,
794 UsageState& st) {
795 const std::string tmp_path = path + ".tmp";
796
797#ifndef _WIN32
798 // CWE-59: Improper Link Resolution Before File Access
799 // Refuse to write through symlinks — lstat() detects symlinks without following them.
800 struct stat st_link;
801 if (::lstat(tmp_path.c_str(), &st_link) == 0 && S_ISLNK(st_link.st_mode)) {
802 return false;
803 }
804 if (::lstat(path.c_str(), &st_link) == 0 && S_ISLNK(st_link.st_mode)) {
805 return false;
806 }
807#endif
808
809 std::ofstream out;
810#ifndef _WIN32
811 // CWE-732: Create with explicit 0600 permissions to prevent world-readable state files.
812 int sfd = ::open(tmp_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600);
813 if (sfd < 0) return false;
814 // Wrap the fd in a stdio FILE*, then attach to ofstream via the path
815 // (fd ownership: we close it after ofstream opens the same path).
816 ::close(sfd);
817 out.open(tmp_path, std::ios::out | std::ios::trunc);
818#else
819 out.open(tmp_path, std::ios::out | std::ios::trunc);
820#endif
821 if (!out.is_open()) return false;
822
823 out << "month_tag=" << st.month_tag << '\n';
824 out << "rows_this_month=" << st.rows_this_month << '\n';
825 out << "eval_start_day_utc=" << st.eval_start_day_utc << '\n';
826 out << "warn_pct_1_emitted=" << (st.warn_pct_1_emitted ? 1 : 0) << '\n';
827 out << "warn_pct_2_emitted=" << (st.warn_pct_2_emitted ? 1 : 0) << '\n';
828 out << "users=" << join_set_csv(st.users) << '\n';
829 out << "nodes=" << join_set_csv(st.nodes) << '\n';
830
831 out.close();
832 if (!out) {
833 std::remove(tmp_path.c_str());
834 return false;
835 }
836
837 if (std::rename(tmp_path.c_str(), path.c_str()) != 0) {
838 std::remove(tmp_path.c_str());
839 return false;
840 }
841
842 st.last_persisted_rows_this_month = st.rows_this_month;
843 return true;
844}
845
854[[nodiscard]] inline LicensePolicy resolve_policy() {
855 LicensePolicy policy;
856
857 const char* license_key = std::getenv(kLicenseEnvVar);
858 const auto claims = parse_claims(license_key ? std::string(license_key) : std::string());
859
860 std::string tier;
861 if (auto it = claims.find("tier"); it != claims.end()) {
862 tier = to_lower_ascii(it->second);
863 } else {
864 const char* env_tier = std::getenv(kLicenseTierEnvVar);
865 if (env_tier != nullptr && env_tier[0] != '\0') {
866 tier = to_lower_ascii(env_tier);
867 }
868 }
869
870 policy.evaluation_mode =
871 (tier == "eval" || tier == "evaluation" || tier == "trial" ||
872 tier == "testing" || tier == "test");
873
874 if (!policy.evaluation_mode) {
875 return policy;
876 }
877
878 policy.max_rows_month = kDefaultEvalMaxRowsMonth;
879 policy.max_users = kDefaultEvalMaxUsers;
880 policy.max_nodes = kDefaultEvalMaxNodes;
881 policy.max_eval_days = kDefaultEvalMaxDays;
882 policy.warn_pct_1 = kDefaultEvalWarnPct1;
883 policy.warn_pct_2 = kDefaultEvalWarnPct2;
884
885 auto set_u64 = [&](const char* key, uint64_t& out) {
886 if (auto it = claims.find(key); it != claims.end()) {
887 uint64_t parsed = 0;
888 if (parse_u64(it->second, parsed)) out = parsed;
889 }
890 };
891
892 auto set_u32 = [&](const char* key, uint32_t& out) {
893 if (auto it = claims.find(key); it != claims.end()) {
894 uint32_t parsed = 0;
895 if (parse_u32(it->second, parsed)) out = parsed;
896 }
897 };
898
899 set_u64("max_rows_month", policy.max_rows_month);
900 set_u64("rows_month", policy.max_rows_month);
901 set_u32("max_users", policy.max_users);
902 set_u32("max_nodes", policy.max_nodes);
903 set_u32("max_days", policy.max_eval_days);
904 set_u32("warn_pct_1", policy.warn_pct_1);
905 set_u32("warn_pct_2", policy.warn_pct_2);
906
907 if (auto it = claims.find("expires_at"); it != claims.end()) {
908 int64_t day = 0;
909 if (parse_iso_date_to_epoch_day(it->second, day)) {
910 policy.explicit_expiry_day_utc = day;
911 }
912 }
913
914 if (policy.warn_pct_1 > 100u) policy.warn_pct_1 = 100u;
915 if (policy.warn_pct_2 > 100u) policy.warn_pct_2 = 100u;
916 if (policy.warn_pct_1 > policy.warn_pct_2) {
917 std::swap(policy.warn_pct_1, policy.warn_pct_2);
918 }
919
920 return policy;
921}
922
931[[nodiscard]] inline expected<void> validate_license_once() {
932#if !defined(SIGNET_ENABLE_COMMERCIAL) || !SIGNET_ENABLE_COMMERCIAL
934 "commercial feature disabled in this Apache build; "
935 "rebuild with -DSIGNET_ENABLE_COMMERCIAL=ON (AGPL-3.0 commercial tier)"};
936#else
937# if defined(SIGNET_REQUIRE_COMMERCIAL_LICENSE) && SIGNET_REQUIRE_COMMERCIAL_LICENSE
938# if !defined(SIGNET_COMMERCIAL_LICENSE_HASH_U64)
940 "commercial tier misconfigured: missing SIGNET_COMMERCIAL_LICENSE_HASH_U64"};
941# else
942 const char* license_key = std::getenv(kLicenseEnvVar);
943 if (license_key == nullptr || license_key[0] == '\0') {
945 std::string("missing runtime license key: set ") + kLicenseEnvVar};
946 }
947
948 constexpr uint64_t kExpectedHash =
949 static_cast<uint64_t>(SIGNET_COMMERCIAL_LICENSE_HASH_U64);
950 const uint64_t actual_hash = fnv1a64(license_key, std::strlen(license_key));
951
952 if (actual_hash != kExpectedHash) {
954 std::string("invalid runtime license key in ") + kLicenseEnvVar};
955 }
956# endif
957# endif
958
959 return expected<void>{};
960#endif
961}
962
964inline std::mutex& usage_state_mutex() {
965 static std::mutex m;
966 return m;
967}
968
970inline UsageState& usage_state() {
971 static UsageState s;
972 return s;
973}
974
976inline void ensure_usage_state_loaded_locked(UsageState& st) {
977 if (st.initialized) return;
978
979 load_usage_state_from_file(usage_state_path(), st);
980 if (st.month_tag == 0) {
981 st.month_tag = current_month_tag_utc();
982 }
983 st.initialized = true;
984}
985
1002[[nodiscard]] inline expected<void> enforce_eval_limits(const char* feature_name,
1003 uint64_t rows_increment) {
1004 static const LicensePolicy policy = resolve_policy();
1005 if (!policy.evaluation_mode) {
1006 return expected<void>{};
1007 }
1008
1009 std::lock_guard<std::mutex> lock(usage_state_mutex());
1010 UsageState& st = usage_state();
1011 ensure_usage_state_loaded_locked(st);
1012
1013 bool persist_required = false;
1014
1015 const int current_month = current_month_tag_utc();
1016 if (st.month_tag != current_month) {
1017 st.month_tag = current_month;
1018 st.rows_this_month = 0;
1019 st.last_persisted_rows_this_month = 0;
1020 st.warn_pct_1_emitted = false;
1021 st.warn_pct_2_emitted = false;
1022 persist_required = true;
1023 }
1024
1025 const int64_t today = current_epoch_day_utc();
1026
1027 if (policy.max_eval_days > 0 && st.eval_start_day_utc == 0) {
1028 st.eval_start_day_utc = today;
1029 persist_required = true;
1030 }
1031
1032 int64_t expiry_day = policy.explicit_expiry_day_utc;
1033 if (expiry_day == 0 && policy.max_eval_days > 0 && st.eval_start_day_utc > 0) {
1034 expiry_day = st.eval_start_day_utc + static_cast<int64_t>(policy.max_eval_days) - 1;
1035 }
1036
1037 if (expiry_day > 0 && today > expiry_day) {
1039 std::string(feature_name) +
1040 ": evaluation period expired; commercial license required"};
1041 }
1042
1043 const std::string user_id = detect_runtime_user();
1044 if (st.users.insert(user_id).second) {
1045 persist_required = true;
1046 }
1047 if (policy.max_users > 0 && st.users.size() > policy.max_users) {
1049 std::string(feature_name) +
1050 ": evaluation user threshold exceeded; commercial license required"};
1051 }
1052
1053 const std::string node_id = detect_runtime_node();
1054 if (st.nodes.insert(node_id).second) {
1055 persist_required = true;
1056 }
1057 if (policy.max_nodes > 0 && st.nodes.size() > policy.max_nodes) {
1059 std::string(feature_name) +
1060 ": evaluation node threshold exceeded; commercial license required"};
1061 }
1062
1063 if (policy.max_rows_month > 0) {
1064 if (rows_increment > 0) {
1065 if (st.rows_this_month >
1066 (std::numeric_limits<uint64_t>::max)() - rows_increment) {
1068 std::string(feature_name) +
1069 ": usage counter overflow; commercial license required"};
1070 }
1071
1072 const uint64_t projected = st.rows_this_month + rows_increment;
1073 if (projected > policy.max_rows_month) {
1075 std::string(feature_name) +
1076 ": evaluation monthly row threshold exceeded; "
1077 "commercial license required"};
1078 }
1079
1080 st.rows_this_month = projected;
1081 if ((st.rows_this_month - st.last_persisted_rows_this_month)
1082 >= kUsagePersistIntervalRows ||
1083 st.rows_this_month == policy.max_rows_month) {
1084 persist_required = true;
1085 }
1086 } else if (st.rows_this_month >= policy.max_rows_month) {
1088 std::string(feature_name) +
1089 ": evaluation monthly row threshold reached; "
1090 "commercial license required"};
1091 }
1092
1093 const long double pct =
1094 (static_cast<long double>(st.rows_this_month) * 100.0L) /
1095 static_cast<long double>(policy.max_rows_month);
1096
1097 if (policy.warn_pct_1 > 0 && !st.warn_pct_1_emitted &&
1098 pct >= static_cast<long double>(policy.warn_pct_1)) {
1099 std::fprintf(stderr,
1100 "signet commercial eval warning: %.2Lf%% of monthly row "
1101 "limit used (%llu/%llu)\n",
1102 pct,
1103 static_cast<unsigned long long>(st.rows_this_month),
1104 static_cast<unsigned long long>(policy.max_rows_month));
1105 st.warn_pct_1_emitted = true;
1106 persist_required = true;
1107 }
1108
1109 if (policy.warn_pct_2 > 0 && !st.warn_pct_2_emitted &&
1110 pct >= static_cast<long double>(policy.warn_pct_2)) {
1111 std::fprintf(stderr,
1112 "signet commercial eval warning: %.2Lf%% of monthly row "
1113 "limit used (%llu/%llu)\n",
1114 pct,
1115 static_cast<unsigned long long>(st.rows_this_month),
1116 static_cast<unsigned long long>(policy.max_rows_month));
1117 st.warn_pct_2_emitted = true;
1118 persist_required = true;
1119 }
1120 }
1121
1122 if (persist_required && !persist_usage_state_to_file(usage_state_path(), st)) {
1124 std::string(feature_name) +
1125 ": unable to persist evaluation usage state"};
1126 }
1127
1128 return expected<void>{};
1129}
1130
1142[[nodiscard]] inline expected<void> require_feature(const char* feature_name,
1143 uint64_t usage_rows = 0) {
1144 static const expected<void> gate = validate_license_once();
1145 if (!gate) {
1146 return Error{gate.error().code,
1147 std::string(feature_name) + ": " + gate.error().message};
1148 }
1149
1150 auto policy_gate = enforce_eval_limits(feature_name, usage_rows);
1151 if (!policy_gate) {
1152 return policy_gate.error();
1153 }
1154
1155 return expected<void>{};
1156}
1157
1168[[nodiscard]] inline expected<void> record_usage_rows(const char* feature_name,
1169 uint64_t rows) {
1170 return require_feature(feature_name, rows);
1171}
1172
1173} // namespace commercial
1174
1175} // namespace signet::forge
1176
expected(Error &&err)
Construct a failed result by moving an Error.
Definition error.hpp:248
const Error & error() const
Access the error payload (valid for both success and failure; check ok() on the returned Error).
Definition error.hpp:261
bool has_value() const
Return true if the result represents success (no error).
Definition error.hpp:255
expected(const Error &err)
Construct a failed result from a const Error reference.
Definition error.hpp:245
expected(ErrorCode code, std::string msg)
Convenience constructor: build an Error in-place from a code and message.
Definition error.hpp:252
expected()
Construct a successful (OK) result with no payload.
Definition error.hpp:242
A lightweight result type that holds either a success value of type T or an Error.
Definition error.hpp:145
expected(T &&val)
Construct a successful result by moving the value.
Definition error.hpp:153
const Error & error() const
Access the error payload.
Definition error.hpp:199
const T & value() const &
Access the success value (const lvalue reference).
Definition error.hpp:177
T & value() &
Access the success value (mutable lvalue reference).
Definition error.hpp:184
expected(const T &val)
Construct a successful result by copying the value (enabled only for copyable types).
Definition error.hpp:149
expected(ErrorCode code, std::string msg)
Convenience constructor: build an Error in-place from a code and message.
Definition error.hpp:165
T && operator*() &&
Dereference the success value (rvalue, moves out).
Definition error.hpp:212
expected(Error &&err)
Construct a failed result by moving an Error.
Definition error.hpp:160
bool has_value() const
Return true if the result holds a success value, false if it holds an Error.
Definition error.hpp:169
const T & operator*() const &
Dereference the success value (const lvalue).
Definition error.hpp:206
T && value() &&
Move-access the success value (rvalue reference).
Definition error.hpp:191
T * operator->()
Arrow operator for member access on the success value (mutable).
Definition error.hpp:218
const T * operator->() const
Arrow operator for member access on the success value (const).
Definition error.hpp:215
T & operator*() &
Dereference the success value (mutable lvalue).
Definition error.hpp:209
expected(const Error &err)
Construct a failed result from a const Error reference.
Definition error.hpp:157
ErrorCode
Error codes returned by all Signet Forge operations.
Definition error.hpp:49
@ HASH_CHAIN_BROKEN
The cryptographic audit hash chain is broken, indicating data tampering.
@ LICENSE_LIMIT_EXCEEDED
An evaluation-tier usage limit has been exceeded (rows, users, nodes, or time).
@ IO_ERROR
A file-system or stream I/O operation failed (open, read, write, rename).
@ ENCRYPTION_ERROR
An encryption or decryption operation failed (bad key, tampered ciphertext, PME error).
@ UNSUPPORTED_COMPRESSION
The file uses a compression codec not linked into this build (ZSTD, LZ4, Gzip).
@ LICENSE_ERROR
The commercial license is missing, invalid, or the build is misconfigured.
@ UNSUPPORTED_TYPE
The file contains a Parquet physical or logical type that is not implemented.
@ OUT_OF_RANGE
An index, offset, or size value is outside the valid range.
@ CORRUPT_FOOTER
The Parquet footer (FileMetaData) is missing, truncated, or malformed.
@ SCHEMA_MISMATCH
The requested column name or type does not match the file schema.
@ INVALID_FILE
The file is not a valid Parquet file (e.g. missing or wrong magic bytes).
@ THRIFT_DECODE_ERROR
The Thrift Compact Protocol decoder encountered invalid or malicious input.
@ UNSUPPORTED_ENCODING
The file uses an encoding not supported by this build (e.g. BYTE_STREAM_SPLIT on integers).
@ OK
Operation completed successfully (no error).
@ INTERNAL_ERROR
An unexpected internal error that does not fit any other category.
@ INVALID_ARGUMENT
A caller-supplied argument is outside the valid range or violates a precondition.
@ CORRUPT_PAGE
A data page failed integrity checks (bad CRC, truncated, or exceeds size limits).
@ CORRUPT_DATA
Decoded data is corrupt or inconsistent (e.g. out-of-range dictionary index).
Lightweight error value carrying an ErrorCode and a human-readable message.
Definition error.hpp:101
bool ok() const
Return true if this error represents success (code == OK).
Definition error.hpp:115
Error()
Construct a default (OK) error with no message.
Definition error.hpp:108
std::string message
A human-readable description of what went wrong (may be empty for OK).
Definition error.hpp:105
Error(ErrorCode c, std::string msg)
Construct an error with the given code and descriptive message.
Definition error.hpp:112
ErrorCode code
The machine-readable error category.
Definition error.hpp:103