Signet Forge 0.1.1
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 <fstream>
16#include <limits>
17#include <mutex>
18#include <sstream>
19#include <string>
20#include <type_traits>
21#include <unordered_map>
22#include <utility>
23#include <unordered_set>
24#include <variant>
25#include <vector>
26
27#ifndef _WIN32
28#include <fcntl.h>
29#include <pwd.h>
30#include <sys/stat.h>
31#include <unistd.h>
32#endif
33
34namespace signet::forge {
35
36// ---------------------------------------------------------------------------
37// Error type — all signet operations return expected<T, Error>
38// ---------------------------------------------------------------------------
39
85
99struct Error {
103 std::string message;
104
110 Error(ErrorCode c, std::string msg) : code(c), message(std::move(msg)) {}
111
113 [[nodiscard]] bool ok() const { return code == ErrorCode::OK; }
116 [[nodiscard]] explicit operator bool() const { return !ok(); }
117};
118
119// ---------------------------------------------------------------------------
120// expected<T, Error> — lightweight result type
121// ---------------------------------------------------------------------------
122
142template <typename T>
143class expected {
144public:
147 expected(const T& val) requires std::is_copy_constructible_v<T>
148 : storage_(val) {}
151 expected(T&& val) : storage_(std::move(val)) {}
152
155 expected(const Error& err) : storage_(err) {}
158 expected(Error&& err) : storage_(std::move(err)) {}
159
163 expected(ErrorCode code, std::string msg)
164 : storage_(Error{code, std::move(msg)}) {}
165
167 [[nodiscard]] bool has_value() const { return std::holds_alternative<T>(storage_); }
170 [[nodiscard]] explicit operator bool() const { return has_value(); }
171
175 [[nodiscard]] const T& value() const& {
176 if (!has_value()) throw std::logic_error("expected::value() called on error");
177 return std::get<T>(storage_);
178 }
182 [[nodiscard]] T& value() & {
183 if (!has_value()) throw std::logic_error("expected::value() called on error");
184 return std::get<T>(storage_);
185 }
189 [[nodiscard]] T&& value() && {
190 if (!has_value()) throw std::logic_error("expected::value() called on error");
191 return std::get<T>(std::move(storage_));
192 }
193
197 [[nodiscard]] const Error& error() const {
198 if (has_value()) throw std::logic_error("expected::error() called on value");
199 return std::get<Error>(storage_);
200 }
201
204 [[nodiscard]] const T& operator*() const& { return value(); }
207 [[nodiscard]] T& operator*() & { return value(); }
210 [[nodiscard]] T&& operator*() && { return std::move(*this).value(); }
213 [[nodiscard]] const T* operator->() const { return &value(); }
216 [[nodiscard]] T* operator->() { return &value(); }
217
218private:
219 std::variant<T, Error> storage_;
220};
221
236template <>
237class expected<void> {
238public:
240 expected() : err_() {}
243 expected(const Error& err) : err_(err) {}
246 expected(Error&& err) : err_(std::move(err)) {}
250 expected(ErrorCode code, std::string msg) : err_(Error{code, std::move(msg)}) {}
251
253 [[nodiscard]] bool has_value() const { return err_.ok(); }
256 [[nodiscard]] explicit operator bool() const { return has_value(); }
259 [[nodiscard]] const Error& error() const { return err_; }
260
261private:
262 Error err_;
263};
264
265} // namespace signet::forge
266
267namespace signet::forge {
268
274namespace commercial {
275
277inline constexpr const char* kLicenseEnvVar = "SIGNET_COMMERCIAL_LICENSE_KEY";
279inline constexpr const char* kLicenseTierEnvVar = "SIGNET_COMMERCIAL_LICENSE_TIER";
281inline constexpr const char* kUsageFileEnvVar = "SIGNET_COMMERCIAL_USAGE_FILE";
283inline constexpr const char* kRuntimeUserEnvVar = "SIGNET_COMMERCIAL_RUNTIME_USER";
285inline constexpr const char* kRuntimeNodeEnvVar = "SIGNET_COMMERCIAL_RUNTIME_NODE";
287inline constexpr uint64_t kUsagePersistIntervalRows = 10'000;
288
291#if defined(SIGNET_EVAL_MAX_ROWS_MONTH_U64)
292inline constexpr uint64_t kDefaultEvalMaxRowsMonth =
293 static_cast<uint64_t>(SIGNET_EVAL_MAX_ROWS_MONTH_U64);
294#else
295inline constexpr uint64_t kDefaultEvalMaxRowsMonth = 50'000'000ull;
296#endif
297
300#if defined(SIGNET_EVAL_MAX_USERS_U32)
301inline constexpr uint32_t kDefaultEvalMaxUsers =
302 static_cast<uint32_t>(SIGNET_EVAL_MAX_USERS_U32);
303#else
304inline constexpr uint32_t kDefaultEvalMaxUsers = 3u;
305#endif
306
309#if defined(SIGNET_EVAL_MAX_NODES_U32)
310inline constexpr uint32_t kDefaultEvalMaxNodes =
311 static_cast<uint32_t>(SIGNET_EVAL_MAX_NODES_U32);
312#else
313inline constexpr uint32_t kDefaultEvalMaxNodes = 1u;
314#endif
315
318#if defined(SIGNET_EVAL_MAX_DAYS_U32)
319inline constexpr uint32_t kDefaultEvalMaxDays =
320 static_cast<uint32_t>(SIGNET_EVAL_MAX_DAYS_U32);
321#else
322inline constexpr uint32_t kDefaultEvalMaxDays = 30u;
323#endif
324
327#if defined(SIGNET_EVAL_WARN_PCT_1_U32)
328inline constexpr uint32_t kDefaultEvalWarnPct1 =
329 static_cast<uint32_t>(SIGNET_EVAL_WARN_PCT_1_U32);
330#else
331inline constexpr uint32_t kDefaultEvalWarnPct1 = 80u;
332#endif
333
336#if defined(SIGNET_EVAL_WARN_PCT_2_U32)
337inline constexpr uint32_t kDefaultEvalWarnPct2 =
338 static_cast<uint32_t>(SIGNET_EVAL_WARN_PCT_2_U32);
339#else
340inline constexpr uint32_t kDefaultEvalWarnPct2 = 90u;
341#endif
342
350struct LicensePolicy {
352 bool evaluation_mode{false};
354 uint64_t max_rows_month{0};
356 uint32_t max_users{0};
358 uint32_t max_nodes{0};
360 int64_t explicit_expiry_day_utc{0};
362 uint32_t max_eval_days{0};
364 uint32_t warn_pct_1{kDefaultEvalWarnPct1};
366 uint32_t warn_pct_2{kDefaultEvalWarnPct2};
367};
368
377struct UsageState {
379 bool initialized{false};
381 int month_tag{0};
383 uint64_t rows_this_month{0};
385 uint64_t last_persisted_rows_this_month{0};
387 int64_t eval_start_day_utc{0};
389 bool warn_pct_1_emitted{false};
391 bool warn_pct_2_emitted{false};
393 std::unordered_set<std::string> users;
395 std::unordered_set<std::string> nodes;
396};
397
404[[nodiscard]] inline uint64_t fnv1a64(const char* data, size_t size) noexcept {
405 constexpr uint64_t kOffset = 14695981039346656037ull;
406 constexpr uint64_t kPrime = 1099511628211ull;
407
408 uint64_t hash = kOffset;
409 for (size_t i = 0; i < size; ++i) {
410 hash ^= static_cast<uint8_t>(data[i]);
411 hash *= kPrime;
412 }
413 return hash;
414}
415
417[[nodiscard]] inline std::string trim_copy(const std::string& in) {
418 size_t start = 0;
419 while (start < in.size() && std::isspace(static_cast<unsigned char>(in[start]))) {
420 ++start;
421 }
422
423 size_t end = in.size();
424 while (end > start && std::isspace(static_cast<unsigned char>(in[end - 1]))) {
425 --end;
426 }
427
428 return in.substr(start, end - start);
429}
430
432[[nodiscard]] inline std::string to_lower_ascii(std::string value) {
433 for (char& ch : value) {
434 ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
435 }
436 return value;
437}
438
440[[nodiscard]] inline std::string sanitize_identity(std::string value,
441 const char* fallback) {
442 value = trim_copy(value);
443 if (value.empty()) {
444 return fallback;
445 }
446
447 if (value.size() > 128) {
448 value.resize(128);
449 }
450
451 for (char& c : value) {
452 if (std::isalnum(static_cast<unsigned char>(c)) ||
453 c == '-' || c == '_' || c == '.' || c == '@') {
454 continue;
455 }
456 c = '_';
457 }
458 return value;
459}
460
462[[nodiscard]] inline std::unordered_map<std::string, std::string>
463parse_claims(const std::string& text) {
464 std::unordered_map<std::string, std::string> out;
465 std::stringstream ss(text);
466 std::string token;
467
468 while (std::getline(ss, token, ';')) {
469 auto pos = token.find('=');
470 if (pos == std::string::npos || pos == 0 || pos + 1 >= token.size()) {
471 continue;
472 }
473 std::string key = to_lower_ascii(trim_copy(token.substr(0, pos)));
474 std::string val = trim_copy(token.substr(pos + 1));
475 if (!key.empty() && !val.empty()) {
476 out[key] = val;
477 }
478 }
479
480 return out;
481}
482
484[[nodiscard]] inline bool parse_u64(const std::string& text, uint64_t& out) {
485 if (text.empty()) return false;
486
487 uint64_t value = 0;
488 for (char ch : text) {
489 if (!std::isdigit(static_cast<unsigned char>(ch))) return false;
490 const uint64_t digit = static_cast<uint64_t>(ch - '0');
491 if (value > ((std::numeric_limits<uint64_t>::max)() - digit) / 10ull) {
492 return false;
493 }
494 value = (value * 10ull) + digit;
495 }
496
497 out = value;
498 return true;
499}
500
502[[nodiscard]] inline bool parse_u32(const std::string& text, uint32_t& out) {
503 uint64_t value = 0;
504 if (!parse_u64(text, value) || value > (std::numeric_limits<uint32_t>::max)()) {
505 return false;
506 }
507 out = static_cast<uint32_t>(value);
508 return true;
509}
510
512[[nodiscard]] inline constexpr int64_t days_from_civil(int y, unsigned m,
513 unsigned d) noexcept {
514 y -= m <= 2;
515 const int era = (y >= 0 ? y : y - 399) / 400;
516 const unsigned yoe = static_cast<unsigned>(y - era * 400);
517 const unsigned mp = (m > 2u) ? (m - 3u) : (m + 9u);
518 const unsigned doy = (153u * mp + 2u) / 5u + d - 1u;
519 const unsigned doe = yoe * 365u + yoe / 4u - yoe / 100u + doy;
520 return static_cast<int64_t>(era) * 146097 + static_cast<int64_t>(doe) - 719468;
521}
522
524[[nodiscard]] inline bool is_leap_year(int y) noexcept {
525 return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
526}
527
529[[nodiscard]] inline bool parse_iso_date_to_epoch_day(const std::string& iso,
530 int64_t& out_day) {
531 if (iso.size() != 10 || iso[4] != '-' || iso[7] != '-') return false;
532
533 auto parse_two = [&](size_t idx, int& out) -> bool {
534 if (!std::isdigit(static_cast<unsigned char>(iso[idx])) ||
535 !std::isdigit(static_cast<unsigned char>(iso[idx + 1]))) {
536 return false;
537 }
538 out = (iso[idx] - '0') * 10 + (iso[idx + 1] - '0');
539 return true;
540 };
541
542 int year = 0;
543 for (size_t i = 0; i < 4; ++i) {
544 if (!std::isdigit(static_cast<unsigned char>(iso[i]))) return false;
545 year = year * 10 + (iso[i] - '0');
546 }
547
548 int month = 0;
549 int day = 0;
550 if (!parse_two(5, month) || !parse_two(8, day)) return false;
551 if (month < 1 || month > 12) return false;
552
553 static constexpr int kMonthDays[] = {31,28,31,30,31,30,31,31,30,31,30,31};
554 int max_day = kMonthDays[month - 1];
555 if (month == 2 && is_leap_year(year)) max_day = 29;
556 if (day < 1 || day > max_day) return false;
557
558 out_day = days_from_civil(year, static_cast<unsigned>(month), static_cast<unsigned>(day));
559 return true;
560}
561
563[[nodiscard]] inline int64_t current_epoch_day_utc() {
564 using namespace std::chrono;
565 const auto now_days = floor<days>(system_clock::now());
566 return now_days.time_since_epoch().count();
567}
568
570[[nodiscard]] inline int current_month_tag_utc() {
571 using namespace std::chrono;
572 const auto now_days = floor<days>(system_clock::now());
573 const year_month_day ymd{now_days};
574 return static_cast<int>(static_cast<int>(ymd.year()) * 100 +
575 static_cast<unsigned>(ymd.month()));
576}
577
582[[nodiscard]] inline std::string default_usage_state_path() {
583 // Prefer XDG_STATE_HOME (e.g. ~/.local/state)
584 const char* xdg = std::getenv("XDG_STATE_HOME");
585 if (xdg && xdg[0]) {
586 return std::string(xdg) + "/signet-forge/usage_state";
587 }
588 // Fall back to HOME-based path
589 const char* home = std::getenv("HOME");
590#ifdef _WIN32
591 if (!home || !home[0]) home = std::getenv("USERPROFILE");
592#endif
593 if (home && home[0]) {
594 return std::string(home) + "/.local/state/signet-forge/usage_state";
595 }
596 // Last resort: /tmp (less secure but functional)
597#if defined(SIGNET_COMMERCIAL_LICENSE_HASH_U64)
598 char buf[96];
599 std::snprintf(buf, sizeof(buf),
600 "/tmp/signet_commercial_usage_%016llx.state",
601 static_cast<unsigned long long>(
602 static_cast<uint64_t>(SIGNET_COMMERCIAL_LICENSE_HASH_U64)));
603 return std::string(buf);
604#else
605 return "/tmp/signet_commercial_usage.state";
606#endif
607}
608
615[[nodiscard]] inline std::string usage_state_path() {
616 return default_usage_state_path();
617}
618
620[[nodiscard]] inline std::string detect_runtime_user() {
621#if defined(SIGNET_ALLOW_RUNTIME_IDENTITY_ENV) && SIGNET_ALLOW_RUNTIME_IDENTITY_ENV
622 const char* env_user = std::getenv(kRuntimeUserEnvVar);
623 if (env_user != nullptr && env_user[0] != '\0') {
624 return sanitize_identity(std::string(env_user), "unknown-user");
625 }
626#endif
627
628#ifndef _WIN32
629 struct passwd pwd_buf {};
630 struct passwd* pwd = nullptr;
631 char storage[4096] = {};
632 if (::getpwuid_r(::geteuid(), &pwd_buf, storage, sizeof(storage), &pwd) == 0 &&
633 pwd != nullptr && pwd->pw_name != nullptr && pwd->pw_name[0] != '\0') {
634 return sanitize_identity(std::string(pwd->pw_name), "unknown-user");
635 }
636
637 const char* user = std::getenv("USER");
638 if (user == nullptr || user[0] == '\0') {
639 user = std::getenv("LOGNAME");
640 }
641#else
642 const char* user = std::getenv("USERNAME");
643#endif
644 return sanitize_identity(user ? std::string(user) : std::string(), "unknown-user");
645}
646
648[[nodiscard]] inline std::string detect_runtime_node() {
649#if defined(SIGNET_ALLOW_RUNTIME_IDENTITY_ENV) && SIGNET_ALLOW_RUNTIME_IDENTITY_ENV
650 const char* env_node = std::getenv(kRuntimeNodeEnvVar);
651 if (env_node != nullptr && env_node[0] != '\0') {
652 return sanitize_identity(std::string(env_node), "unknown-node");
653 }
654#endif
655
656#ifndef _WIN32
657 char host[256] = {};
658 if (::gethostname(host, sizeof(host) - 1) == 0) {
659 host[sizeof(host) - 1] = '\0';
660 return sanitize_identity(std::string(host), "unknown-node");
661 }
662 const char* node = std::getenv("HOSTNAME");
663#else
664 const char* node = std::getenv("COMPUTERNAME");
665#endif
666 return sanitize_identity(node ? std::string(node) : std::string(), "unknown-node");
667}
668
670[[nodiscard]] inline std::vector<std::string> split_csv(const std::string& text) {
671 std::vector<std::string> out;
672 std::stringstream ss(text);
673 std::string token;
674
675 while (std::getline(ss, token, ',')) {
676 token = sanitize_identity(token, "");
677 if (!token.empty()) {
678 out.push_back(token);
679 }
680 }
681
682 return out;
683}
684
686[[nodiscard]] inline std::string join_set_csv(const std::unordered_set<std::string>& values) {
687 std::vector<std::string> ordered;
688 ordered.reserve(values.size());
689 for (const auto& v : values) {
690 ordered.push_back(v);
691 }
692 std::sort(ordered.begin(), ordered.end());
693
694 std::string out;
695 for (size_t i = 0; i < ordered.size(); ++i) {
696 if (i != 0) out.push_back(',');
697 out += ordered[i];
698 }
699 return out;
700}
701
703inline void load_usage_state_from_file(const std::string& path, UsageState& st) {
704 std::ifstream in(path);
705 if (!in.good()) {
706 return;
707 }
708
709 std::string line;
710 while (std::getline(in, line)) {
711 auto eq = line.find('=');
712 if (eq == std::string::npos) continue;
713
714 const std::string key = trim_copy(line.substr(0, eq));
715 const std::string val = trim_copy(line.substr(eq + 1));
716
717 uint64_t u64 = 0;
718 if (key == "month_tag" && parse_u64(val, u64)) {
719 st.month_tag = static_cast<int>(u64);
720 } else if (key == "rows_this_month" && parse_u64(val, u64)) {
721 st.rows_this_month = u64;
722 } else if (key == "eval_start_day_utc" && parse_u64(val, u64)) {
723 st.eval_start_day_utc = static_cast<int64_t>(u64);
724 } else if (key == "warn_pct_1_emitted") {
725 st.warn_pct_1_emitted = (val == "1");
726 } else if (key == "warn_pct_2_emitted") {
727 st.warn_pct_2_emitted = (val == "1");
728 } else if (key == "users") {
729 for (auto& token : split_csv(val)) {
730 st.users.insert(token);
731 }
732 } else if (key == "nodes") {
733 for (auto& token : split_csv(val)) {
734 st.nodes.insert(token);
735 }
736 }
737 }
738
739 st.last_persisted_rows_this_month = st.rows_this_month;
740}
741
745[[nodiscard]] inline bool persist_usage_state_to_file(const std::string& path,
746 UsageState& st) {
747 const std::string tmp_path = path + ".tmp";
748
749#ifndef _WIN32
750 // CWE-59: Improper Link Resolution Before File Access
751 // Refuse to write through symlinks — lstat() detects symlinks without following them.
752 struct stat st_link;
753 if (::lstat(tmp_path.c_str(), &st_link) == 0 && S_ISLNK(st_link.st_mode)) {
754 return false;
755 }
756 if (::lstat(path.c_str(), &st_link) == 0 && S_ISLNK(st_link.st_mode)) {
757 return false;
758 }
759#endif
760
761 std::ofstream out;
762#ifndef _WIN32
763 // CWE-732: Create with explicit 0600 permissions to prevent world-readable state files.
764 int sfd = ::open(tmp_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600);
765 if (sfd < 0) return false;
766 // Wrap the fd in a stdio FILE*, then attach to ofstream via the path
767 // (fd ownership: we close it after ofstream opens the same path).
768 ::close(sfd);
769 out.open(tmp_path, std::ios::out | std::ios::trunc);
770#else
771 out.open(tmp_path, std::ios::out | std::ios::trunc);
772#endif
773 if (!out.is_open()) return false;
774
775 out << "month_tag=" << st.month_tag << '\n';
776 out << "rows_this_month=" << st.rows_this_month << '\n';
777 out << "eval_start_day_utc=" << st.eval_start_day_utc << '\n';
778 out << "warn_pct_1_emitted=" << (st.warn_pct_1_emitted ? 1 : 0) << '\n';
779 out << "warn_pct_2_emitted=" << (st.warn_pct_2_emitted ? 1 : 0) << '\n';
780 out << "users=" << join_set_csv(st.users) << '\n';
781 out << "nodes=" << join_set_csv(st.nodes) << '\n';
782
783 out.close();
784 if (!out) {
785 std::remove(tmp_path.c_str());
786 return false;
787 }
788
789 if (std::rename(tmp_path.c_str(), path.c_str()) != 0) {
790 std::remove(tmp_path.c_str());
791 return false;
792 }
793
794 st.last_persisted_rows_this_month = st.rows_this_month;
795 return true;
796}
797
806[[nodiscard]] inline LicensePolicy resolve_policy() {
807 LicensePolicy policy;
808
809 const char* license_key = std::getenv(kLicenseEnvVar);
810 const auto claims = parse_claims(license_key ? std::string(license_key) : std::string());
811
812 std::string tier;
813 if (auto it = claims.find("tier"); it != claims.end()) {
814 tier = to_lower_ascii(it->second);
815 } else {
816 const char* env_tier = std::getenv(kLicenseTierEnvVar);
817 if (env_tier != nullptr && env_tier[0] != '\0') {
818 tier = to_lower_ascii(env_tier);
819 }
820 }
821
822 policy.evaluation_mode =
823 (tier == "eval" || tier == "evaluation" || tier == "trial" ||
824 tier == "testing" || tier == "test");
825
826 if (!policy.evaluation_mode) {
827 return policy;
828 }
829
830 policy.max_rows_month = kDefaultEvalMaxRowsMonth;
831 policy.max_users = kDefaultEvalMaxUsers;
832 policy.max_nodes = kDefaultEvalMaxNodes;
833 policy.max_eval_days = kDefaultEvalMaxDays;
834 policy.warn_pct_1 = kDefaultEvalWarnPct1;
835 policy.warn_pct_2 = kDefaultEvalWarnPct2;
836
837 auto set_u64 = [&](const char* key, uint64_t& out) {
838 if (auto it = claims.find(key); it != claims.end()) {
839 uint64_t parsed = 0;
840 if (parse_u64(it->second, parsed)) out = parsed;
841 }
842 };
843
844 auto set_u32 = [&](const char* key, uint32_t& out) {
845 if (auto it = claims.find(key); it != claims.end()) {
846 uint32_t parsed = 0;
847 if (parse_u32(it->second, parsed)) out = parsed;
848 }
849 };
850
851 set_u64("max_rows_month", policy.max_rows_month);
852 set_u64("rows_month", policy.max_rows_month);
853 set_u32("max_users", policy.max_users);
854 set_u32("max_nodes", policy.max_nodes);
855 set_u32("max_days", policy.max_eval_days);
856 set_u32("warn_pct_1", policy.warn_pct_1);
857 set_u32("warn_pct_2", policy.warn_pct_2);
858
859 if (auto it = claims.find("expires_at"); it != claims.end()) {
860 int64_t day = 0;
861 if (parse_iso_date_to_epoch_day(it->second, day)) {
862 policy.explicit_expiry_day_utc = day;
863 }
864 }
865
866 if (policy.warn_pct_1 > 100u) policy.warn_pct_1 = 100u;
867 if (policy.warn_pct_2 > 100u) policy.warn_pct_2 = 100u;
868 if (policy.warn_pct_1 > policy.warn_pct_2) {
869 std::swap(policy.warn_pct_1, policy.warn_pct_2);
870 }
871
872 return policy;
873}
874
883[[nodiscard]] inline expected<void> validate_license_once() {
884#if !defined(SIGNET_ENABLE_COMMERCIAL) || !SIGNET_ENABLE_COMMERCIAL
886 "commercial feature disabled in this Apache build; "
887 "rebuild with -DSIGNET_ENABLE_COMMERCIAL=ON (AGPL-3.0 commercial tier)"};
888#else
889# if defined(SIGNET_REQUIRE_COMMERCIAL_LICENSE) && SIGNET_REQUIRE_COMMERCIAL_LICENSE
890# if !defined(SIGNET_COMMERCIAL_LICENSE_HASH_U64)
892 "commercial tier misconfigured: missing SIGNET_COMMERCIAL_LICENSE_HASH_U64"};
893# else
894 const char* license_key = std::getenv(kLicenseEnvVar);
895 if (license_key == nullptr || license_key[0] == '\0') {
897 std::string("missing runtime license key: set ") + kLicenseEnvVar};
898 }
899
900 constexpr uint64_t kExpectedHash =
901 static_cast<uint64_t>(SIGNET_COMMERCIAL_LICENSE_HASH_U64);
902 const uint64_t actual_hash = fnv1a64(license_key, std::strlen(license_key));
903
904 if (actual_hash != kExpectedHash) {
906 std::string("invalid runtime license key in ") + kLicenseEnvVar};
907 }
908# endif
909# endif
910
911 return expected<void>{};
912#endif
913}
914
916inline std::mutex& usage_state_mutex() {
917 static std::mutex m;
918 return m;
919}
920
922inline UsageState& usage_state() {
923 static UsageState s;
924 return s;
925}
926
928inline void ensure_usage_state_loaded_locked(UsageState& st) {
929 if (st.initialized) return;
930
931 load_usage_state_from_file(usage_state_path(), st);
932 if (st.month_tag == 0) {
933 st.month_tag = current_month_tag_utc();
934 }
935 st.initialized = true;
936}
937
954[[nodiscard]] inline expected<void> enforce_eval_limits(const char* feature_name,
955 uint64_t rows_increment) {
956 static const LicensePolicy policy = resolve_policy();
957 if (!policy.evaluation_mode) {
958 return expected<void>{};
959 }
960
961 std::lock_guard<std::mutex> lock(usage_state_mutex());
962 UsageState& st = usage_state();
963 ensure_usage_state_loaded_locked(st);
964
965 bool persist_required = false;
966
967 const int current_month = current_month_tag_utc();
968 if (st.month_tag != current_month) {
969 st.month_tag = current_month;
970 st.rows_this_month = 0;
971 st.last_persisted_rows_this_month = 0;
972 st.warn_pct_1_emitted = false;
973 st.warn_pct_2_emitted = false;
974 persist_required = true;
975 }
976
977 const int64_t today = current_epoch_day_utc();
978
979 if (policy.max_eval_days > 0 && st.eval_start_day_utc == 0) {
980 st.eval_start_day_utc = today;
981 persist_required = true;
982 }
983
984 int64_t expiry_day = policy.explicit_expiry_day_utc;
985 if (expiry_day == 0 && policy.max_eval_days > 0 && st.eval_start_day_utc > 0) {
986 expiry_day = st.eval_start_day_utc + static_cast<int64_t>(policy.max_eval_days) - 1;
987 }
988
989 if (expiry_day > 0 && today > expiry_day) {
991 std::string(feature_name) +
992 ": evaluation period expired; commercial license required"};
993 }
994
995 const std::string user_id = detect_runtime_user();
996 if (st.users.insert(user_id).second) {
997 persist_required = true;
998 }
999 if (policy.max_users > 0 && st.users.size() > policy.max_users) {
1001 std::string(feature_name) +
1002 ": evaluation user threshold exceeded; commercial license required"};
1003 }
1004
1005 const std::string node_id = detect_runtime_node();
1006 if (st.nodes.insert(node_id).second) {
1007 persist_required = true;
1008 }
1009 if (policy.max_nodes > 0 && st.nodes.size() > policy.max_nodes) {
1011 std::string(feature_name) +
1012 ": evaluation node threshold exceeded; commercial license required"};
1013 }
1014
1015 if (policy.max_rows_month > 0) {
1016 if (rows_increment > 0) {
1017 if (st.rows_this_month >
1018 (std::numeric_limits<uint64_t>::max)() - rows_increment) {
1020 std::string(feature_name) +
1021 ": usage counter overflow; commercial license required"};
1022 }
1023
1024 const uint64_t projected = st.rows_this_month + rows_increment;
1025 if (projected > policy.max_rows_month) {
1027 std::string(feature_name) +
1028 ": evaluation monthly row threshold exceeded; "
1029 "commercial license required"};
1030 }
1031
1032 st.rows_this_month = projected;
1033 if ((st.rows_this_month - st.last_persisted_rows_this_month)
1034 >= kUsagePersistIntervalRows ||
1035 st.rows_this_month == policy.max_rows_month) {
1036 persist_required = true;
1037 }
1038 } else if (st.rows_this_month >= policy.max_rows_month) {
1040 std::string(feature_name) +
1041 ": evaluation monthly row threshold reached; "
1042 "commercial license required"};
1043 }
1044
1045 const long double pct =
1046 (static_cast<long double>(st.rows_this_month) * 100.0L) /
1047 static_cast<long double>(policy.max_rows_month);
1048
1049 if (policy.warn_pct_1 > 0 && !st.warn_pct_1_emitted &&
1050 pct >= static_cast<long double>(policy.warn_pct_1)) {
1051 std::fprintf(stderr,
1052 "signet commercial eval warning: %.2Lf%% of monthly row "
1053 "limit used (%llu/%llu)\n",
1054 pct,
1055 static_cast<unsigned long long>(st.rows_this_month),
1056 static_cast<unsigned long long>(policy.max_rows_month));
1057 st.warn_pct_1_emitted = true;
1058 persist_required = true;
1059 }
1060
1061 if (policy.warn_pct_2 > 0 && !st.warn_pct_2_emitted &&
1062 pct >= static_cast<long double>(policy.warn_pct_2)) {
1063 std::fprintf(stderr,
1064 "signet commercial eval warning: %.2Lf%% of monthly row "
1065 "limit used (%llu/%llu)\n",
1066 pct,
1067 static_cast<unsigned long long>(st.rows_this_month),
1068 static_cast<unsigned long long>(policy.max_rows_month));
1069 st.warn_pct_2_emitted = true;
1070 persist_required = true;
1071 }
1072 }
1073
1074 if (persist_required && !persist_usage_state_to_file(usage_state_path(), st)) {
1076 std::string(feature_name) +
1077 ": unable to persist evaluation usage state"};
1078 }
1079
1080 return expected<void>{};
1081}
1082
1094[[nodiscard]] inline expected<void> require_feature(const char* feature_name,
1095 uint64_t usage_rows = 0) {
1096 static const expected<void> gate = validate_license_once();
1097 if (!gate) {
1098 return Error{gate.error().code,
1099 std::string(feature_name) + ": " + gate.error().message};
1100 }
1101
1102 auto policy_gate = enforce_eval_limits(feature_name, usage_rows);
1103 if (!policy_gate) {
1104 return policy_gate.error();
1105 }
1106
1107 return expected<void>{};
1108}
1109
1120[[nodiscard]] inline expected<void> record_usage_rows(const char* feature_name,
1121 uint64_t rows) {
1122 return require_feature(feature_name, rows);
1123}
1124
1125} // namespace commercial
1126
1127} // namespace signet::forge
1128
expected(Error &&err)
Construct a failed result by moving an Error.
Definition error.hpp:246
const Error & error() const
Access the error payload (valid for both success and failure; check ok() on the returned Error).
Definition error.hpp:259
bool has_value() const
Return true if the result represents success (no error).
Definition error.hpp:253
expected(const Error &err)
Construct a failed result from a const Error reference.
Definition error.hpp:243
expected(ErrorCode code, std::string msg)
Convenience constructor: build an Error in-place from a code and message.
Definition error.hpp:250
expected()
Construct a successful (OK) result with no payload.
Definition error.hpp:240
A lightweight result type that holds either a success value of type T or an Error.
Definition error.hpp:143
expected(T &&val)
Construct a successful result by moving the value.
Definition error.hpp:151
const Error & error() const
Access the error payload.
Definition error.hpp:197
const T & value() const &
Access the success value (const lvalue reference).
Definition error.hpp:175
T & value() &
Access the success value (mutable lvalue reference).
Definition error.hpp:182
expected(const T &val)
Construct a successful result by copying the value (enabled only for copyable types).
Definition error.hpp:147
expected(ErrorCode code, std::string msg)
Convenience constructor: build an Error in-place from a code and message.
Definition error.hpp:163
T && operator*() &&
Dereference the success value (rvalue, moves out).
Definition error.hpp:210
expected(Error &&err)
Construct a failed result by moving an Error.
Definition error.hpp:158
bool has_value() const
Return true if the result holds a success value, false if it holds an Error.
Definition error.hpp:167
const T & operator*() const &
Dereference the success value (const lvalue).
Definition error.hpp:204
T && value() &&
Move-access the success value (rvalue reference).
Definition error.hpp:189
T * operator->()
Arrow operator for member access on the success value (mutable).
Definition error.hpp:216
const T * operator->() const
Arrow operator for member access on the success value (const).
Definition error.hpp:213
T & operator*() &
Dereference the success value (mutable lvalue).
Definition error.hpp:207
expected(const Error &err)
Construct a failed result from a const Error reference.
Definition error.hpp:155
ErrorCode
Error codes returned by all Signet Forge operations.
Definition error.hpp:47
@ 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:99
bool ok() const
Return true if this error represents success (code == OK).
Definition error.hpp:113
Error()
Construct a default (OK) error with no message.
Definition error.hpp:106
std::string message
A human-readable description of what went wrong (may be empty for OK).
Definition error.hpp:103
Error(ErrorCode c, std::string msg)
Construct an error with the given code and descriptive message.
Definition error.hpp:110
ErrorCode code
The machine-readable error category.
Definition error.hpp:101