feat: the crafter-review skeleton, and its semantics for text, JSON and crypto
What the rest of crafter-review stands on. - project.cpp: a static crafter.review-lib and the crafter-review executable; Crafter.Network without QUIC, pinned, or with --local from the checkout next to this one; Crafter.LintRules' lite rules, pinned; the version from CRAFTER_REVIEW_VERSION, git describe or 0.0.0-dev. - :Core and :Driver: the version, PromptVersion 1, --version. - :Json: nlohmann's ordered_json, which keeps key order and unknown keys, so the files every driver on a machine shares survive a rewrite; dumps in the indented file layout, the spaced one-line form and the compact sorted form the structured-output schema needs byte for byte. - :Crypto: SHA-256/SHA-1/MD5, HMAC, PBKDF2 through libcrypto; base64 and base32 written out, because what they refuse, and with which message, is pinned. - :Unicode, Unicode 16.0.0's tables: whitespace, line breaks, decimal digits, printable characters, case mappings and case-insensitive folds. - :Text: string functions on code points, number parsing, the shortest float form and half-even rounding, printed values, shell patterns, URL splitting and unquoting, POSIX paths, universal newlines and the replacing UTF-8 decoder. Tests: Crypto (published vectors), Text (293 reference vectors). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e176dc87b5
commit
36852edf63
16 changed files with 7845 additions and 0 deletions
16
implementations/Crafter.Review-Core.cpp
Normal file
16
implementations/Crafter.Review-Core.cpp
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module Crafter.Review:Core_impl;
|
||||
import :Core;
|
||||
import std;
|
||||
|
||||
namespace Crafter::Review {
|
||||
std::string_view Version() {
|
||||
return CRAFTER_REVIEW_VERSION;
|
||||
}
|
||||
|
||||
std::string DriverLine() {
|
||||
return std::format("crafter-review {} (prompt v{})", Version(), PromptVersion);
|
||||
}
|
||||
}
|
||||
239
implementations/Crafter.Review-Crypto.cpp
Normal file
239
implementations/Crafter.Review-Crypto.cpp
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
#include <openssl/crypto.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/hmac.h>
|
||||
#include <sys/random.h>
|
||||
#include <cerrno>
|
||||
|
||||
module Crafter.Review:Crypto_impl;
|
||||
import :Crypto;
|
||||
import std;
|
||||
|
||||
namespace Crafter::Review {
|
||||
namespace {
|
||||
std::string Digest(const EVP_MD* method, std::string_view data) {
|
||||
std::array<unsigned char, EVP_MAX_MD_SIZE> out {};
|
||||
std::uint32_t length = 0;
|
||||
if (EVP_Digest(data.data(), data.size(), out.data(), &length, method, nullptr) != 1) {
|
||||
throw std::runtime_error("libcrypto: digest failed");
|
||||
}
|
||||
return std::string(reinterpret_cast<const char*>(out.data()), length);
|
||||
}
|
||||
|
||||
std::string Hmac(const EVP_MD* method, std::string_view key, std::string_view data) {
|
||||
std::array<unsigned char, EVP_MAX_MD_SIZE> out {};
|
||||
std::uint32_t length = 0;
|
||||
const unsigned char* result = HMAC(method, key.data(), static_cast<int>(key.size()),
|
||||
reinterpret_cast<const unsigned char*>(data.data()), data.size(),
|
||||
out.data(), &length);
|
||||
if (result == nullptr) throw std::runtime_error("libcrypto: HMAC failed");
|
||||
return std::string(reinterpret_cast<const char*>(out.data()), length);
|
||||
}
|
||||
|
||||
constexpr std::string_view Base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
constexpr std::string_view Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
|
||||
std::int32_t Base64Value(char c) {
|
||||
const std::size_t at = Base64Alphabet.find(c);
|
||||
return at == std::string_view::npos ? -1 : static_cast<std::int32_t>(at);
|
||||
}
|
||||
}
|
||||
|
||||
std::string Sha256(std::string_view data) {
|
||||
return Digest(EVP_sha256(), data);
|
||||
}
|
||||
|
||||
std::string Sha1(std::string_view data) {
|
||||
return Digest(EVP_sha1(), data);
|
||||
}
|
||||
|
||||
std::string Md5(std::string_view data) {
|
||||
return Digest(EVP_md5(), data);
|
||||
}
|
||||
|
||||
std::string Sha256Hex(std::string_view data) {
|
||||
return HexEncode(Sha256(data));
|
||||
}
|
||||
|
||||
std::string Md5Hex(std::string_view data) {
|
||||
return HexEncode(Md5(data));
|
||||
}
|
||||
|
||||
std::string HmacSha256(std::string_view key, std::string_view data) {
|
||||
return Hmac(EVP_sha256(), key, data);
|
||||
}
|
||||
|
||||
std::string HmacSha1(std::string_view key, std::string_view data) {
|
||||
return Hmac(EVP_sha1(), key, data);
|
||||
}
|
||||
|
||||
std::string Pbkdf2Sha256(std::string_view password, std::string_view salt, std::uint32_t iterations) {
|
||||
std::array<unsigned char, 32> out {};
|
||||
if (PKCS5_PBKDF2_HMAC(password.data(), static_cast<std::int32_t>(password.size()),
|
||||
reinterpret_cast<const std::uint8_t*>(salt.data()), static_cast<std::int32_t>(salt.size()),
|
||||
static_cast<std::int32_t>(iterations), EVP_sha256(), static_cast<std::int32_t>(out.size()), out.data()) != 1) {
|
||||
throw std::runtime_error("libcrypto: PBKDF2 failed");
|
||||
}
|
||||
return std::string(reinterpret_cast<const char*>(out.data()), out.size());
|
||||
}
|
||||
|
||||
std::string HexEncode(std::string_view bytes) {
|
||||
constexpr std::string_view Digits = "0123456789abcdef";
|
||||
std::string out;
|
||||
out.reserve(bytes.size() * 2);
|
||||
for (const char c : bytes) {
|
||||
const auto byte = static_cast<std::uint8_t>(c);
|
||||
out.push_back(Digits[byte >> 4]);
|
||||
out.push_back(Digits[byte & 0x0F]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string Base64Encode(std::string_view bytes) {
|
||||
std::string out;
|
||||
out.reserve((bytes.size() + 2) / 3 * 4);
|
||||
std::size_t i = 0;
|
||||
for (; i + 3 <= bytes.size(); i += 3) {
|
||||
const std::uint32_t triple = (static_cast<std::uint8_t>(bytes[i]) << 16) | (static_cast<std::uint8_t>(bytes[i + 1]) << 8) | static_cast<std::uint8_t>(bytes[i + 2]);
|
||||
out.push_back(Base64Alphabet[(triple >> 18) & 0x3F]);
|
||||
out.push_back(Base64Alphabet[(triple >> 12) & 0x3F]);
|
||||
out.push_back(Base64Alphabet[(triple >> 6) & 0x3F]);
|
||||
out.push_back(Base64Alphabet[triple & 0x3F]);
|
||||
}
|
||||
const std::size_t left = bytes.size() - i;
|
||||
if (left == 1) {
|
||||
const std::uint32_t one = static_cast<std::uint8_t>(bytes[i]) << 16;
|
||||
out.push_back(Base64Alphabet[(one >> 18) & 0x3F]);
|
||||
out.push_back(Base64Alphabet[(one >> 12) & 0x3F]);
|
||||
out += "==";
|
||||
} else if (left == 2) {
|
||||
const std::uint32_t two = (static_cast<std::uint8_t>(bytes[i]) << 16) | (static_cast<std::uint8_t>(bytes[i + 1]) << 8);
|
||||
out.push_back(Base64Alphabet[(two >> 18) & 0x3F]);
|
||||
out.push_back(Base64Alphabet[(two >> 12) & 0x3F]);
|
||||
out.push_back(Base64Alphabet[(two >> 6) & 0x3F]);
|
||||
out.push_back('=');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string Base64Decode(std::string_view text) {
|
||||
// binascii.a2b_base64 in its default, non-strict mode, which is what
|
||||
// b64decode runs: a character outside the alphabet is skipped, a '='
|
||||
// only counts once two characters of a quad are in, and a complete
|
||||
// padding sequence ends the data — whatever follows it is ignored.
|
||||
// What a quad has left at the end decides the error.
|
||||
std::string out;
|
||||
std::int32_t quadPosition = 0;
|
||||
std::uint8_t leftChar = 0;
|
||||
std::int32_t pads = 0;
|
||||
for (const char c : text) {
|
||||
if (c == '=') {
|
||||
if (quadPosition >= 2 && quadPosition + ++pads >= 4) return out;
|
||||
continue;
|
||||
}
|
||||
const std::int32_t value = Base64Value(c);
|
||||
if (value < 0) continue;
|
||||
const auto bits = static_cast<std::uint8_t>(value);
|
||||
pads = 0;
|
||||
switch (quadPosition) {
|
||||
case 0:
|
||||
quadPosition = 1;
|
||||
leftChar = bits;
|
||||
break;
|
||||
case 1:
|
||||
quadPosition = 2;
|
||||
out.push_back(static_cast<char>(static_cast<std::uint8_t>(leftChar << 2) | (bits >> 4)));
|
||||
leftChar = bits & 0x0F;
|
||||
break;
|
||||
case 2:
|
||||
quadPosition = 3;
|
||||
out.push_back(static_cast<char>(static_cast<std::uint8_t>(leftChar << 4) | (bits >> 2)));
|
||||
leftChar = bits & 0x03;
|
||||
break;
|
||||
default:
|
||||
quadPosition = 0;
|
||||
out.push_back(static_cast<char>(static_cast<std::uint8_t>(leftChar << 6) | bits));
|
||||
leftChar = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (quadPosition == 1) {
|
||||
throw std::invalid_argument(std::format("Invalid base64-encoded string: number of data characters ({}) cannot be 1 more than a multiple of 4", out.size() / 3 * 4 + 1));
|
||||
}
|
||||
if (quadPosition != 0) throw std::invalid_argument("Incorrect padding");
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string Base32Decode(std::string_view text) {
|
||||
// base64.b32decode, in its order: the digits are checked while the
|
||||
// quanta are decoded, the length and the padding after.
|
||||
std::string_view data = text;
|
||||
while (!data.empty() && data.back() == '=') data.remove_suffix(1);
|
||||
const std::size_t padChars = text.size() - data.size();
|
||||
std::string out;
|
||||
std::uint64_t accumulator = 0;
|
||||
for (std::size_t start = 0; start < data.size(); start += 8) {
|
||||
accumulator = 0;
|
||||
for (const char c : data.substr(start, 8)) {
|
||||
const std::size_t value = Base32Alphabet.find(c);
|
||||
if (value == std::string_view::npos) throw std::invalid_argument("Non-base32 digit found");
|
||||
accumulator = (accumulator << 5) | value;
|
||||
}
|
||||
for (std::int32_t shift = 32; shift >= 0; shift -= 8) {
|
||||
out.push_back(static_cast<char>((accumulator >> shift) & 0xFF));
|
||||
}
|
||||
}
|
||||
if (text.size() % 8 != 0 || (padChars != 0 && padChars != 1 && padChars != 3 && padChars != 4 && padChars != 6)) {
|
||||
throw std::invalid_argument("Incorrect padding");
|
||||
}
|
||||
if (padChars != 0 && !out.empty()) {
|
||||
// b32decode decoded the partial quantum as if it were 8 digits
|
||||
// short of its padding: redo the last 5 bytes from the
|
||||
// accumulator shifted by the padding, and keep what the data
|
||||
// characters carried.
|
||||
const std::uint64_t shifted = accumulator << (5 * padChars);
|
||||
std::string last;
|
||||
for (std::int32_t shift = 32; shift >= 0; shift -= 8) {
|
||||
last.push_back(static_cast<char>((shifted >> shift) & 0xFF));
|
||||
}
|
||||
const std::size_t leftover = (43 - 5 * padChars) / 8;
|
||||
out.resize(out.size() - 5);
|
||||
out += last.substr(0, leftover);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string RandomBytes(std::size_t count) {
|
||||
std::string out(count, '\0');
|
||||
std::size_t filled = 0;
|
||||
while (filled < count) {
|
||||
const ssize_t got = getrandom(out.data() + filled, count - filled, 0);
|
||||
if (got < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
throw std::runtime_error(std::format("getrandom: {}", std::strerror(errno)));
|
||||
}
|
||||
filled += static_cast<std::size_t>(got);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string TokenHex(std::size_t count) {
|
||||
return HexEncode(RandomBytes(count));
|
||||
}
|
||||
|
||||
std::string Uuid4() {
|
||||
std::string bytes = RandomBytes(16);
|
||||
bytes[6] = static_cast<char>((static_cast<std::uint8_t>(bytes[6]) & 0x0F) | 0x40);
|
||||
bytes[8] = static_cast<char>((static_cast<std::uint8_t>(bytes[8]) & 0x3F) | 0x80);
|
||||
const std::string hex = HexEncode(bytes);
|
||||
return std::format("{}-{}-{}-{}-{}", hex.substr(0, 8), hex.substr(8, 4), hex.substr(12, 4), hex.substr(16, 4), hex.substr(20));
|
||||
}
|
||||
|
||||
bool ConstantTimeEqual(std::string_view a, std::string_view b) {
|
||||
if (a.size() != b.size()) return false;
|
||||
return CRYPTO_memcmp(a.data(), b.data(), a.size()) == 0;
|
||||
}
|
||||
}
|
||||
33
implementations/Crafter.Review-Driver.cpp
Normal file
33
implementations/Crafter.Review-Driver.cpp
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module Crafter.Review:Driver_impl;
|
||||
import :Driver;
|
||||
import :Core;
|
||||
import std;
|
||||
|
||||
namespace Crafter::Review {
|
||||
namespace {
|
||||
constexpr std::string_view Usage =
|
||||
R"(usage: crafter-review [--config FILE] {run,freeze,slots,config} ...
|
||||
crafter-review {seats,inventory,sql,totp,init,doc,browser,stop} ...
|
||||
crafter-review --version
|
||||
|
||||
The pre-push review pipeline: adversarial lenses, one claude process per
|
||||
inventory unit, against a frozen commit range.)";
|
||||
}
|
||||
|
||||
std::int32_t Main(std::span<const std::string_view> args) {
|
||||
const std::span<const std::string_view> rest = args.empty() ? args : args.subspan(1);
|
||||
if (!rest.empty() && rest.front() == "--version") {
|
||||
std::println("{}", DriverLine());
|
||||
return 0;
|
||||
}
|
||||
if (!rest.empty() && (rest.front() == "-h" || rest.front() == "--help")) {
|
||||
std::println("{}", Usage);
|
||||
return 0;
|
||||
}
|
||||
std::println(std::cerr, "{}", Usage);
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
1061
implementations/Crafter.Review-Text.cpp
Normal file
1061
implementations/Crafter.Review-Text.cpp
Normal file
File diff suppressed because it is too large
Load diff
10
implementations/main.cpp
Normal file
10
implementations/main.cpp
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
import std;
|
||||
import Crafter.Review;
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
const std::vector<std::string_view> args(argv, argv + argc);
|
||||
return Crafter::Review::Main(args);
|
||||
}
|
||||
Loading…
Reference in a new issue