Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
ckey_conformance.cpp
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | // CKey conformance vectors (specification §6, acceptance criterion 1). |
| 2 | // |
| 3 | // ckey-conformance --write tests/vectors/conformance-v1.json |
| 4 | // ckey-conformance --check tests/vectors/conformance-v1.json |
| 5 | // |
| 6 | // Each vector fixes an input (profile, version, level, layout, payload) and |
| 7 | // the expected outputs: the codeword (hex, plus FNV-1a 64), the canonical |
| 8 | // geometry (shape count and an FNV-1a 64 over every shape with coordinates |
| 9 | // rounded to 1e-6 units), the .ckey file hash, and a behavioural check that |
| 10 | // a binary raster at 8 px/module decodes back to the payload. Any SDK can |
| 11 | // load the same JSON and compare its own results. Hashes are FNV-1a 64 over |
| 12 | // the bytes described in tools/conformance/README.md. |
| 13 | "ckey/ckey.h" |
| 14 | |
| 15 | <cmath> |
| 16 | <cstdio> |
| 17 | <cstring> |
| 18 | <fstream> |
| 19 | <iostream> |
| 20 | <map> |
| 21 | <memory> |
| 22 | <sstream> |
| 23 | <string> |
| 24 | <vector> |
| 25 | |
| 26 | namespace { |
| 27 | |
| 28 | uint64_t fnv(const uint8_t *p, size_t n, uint64_t h = 0xcbf29ce484222325ull) { for (size_t i = 0; i < n; ++i) { h ^= p[i]; h *= 0x100000001b3ull; } return h; } |
| 29 | std::string hex(const uint8_t *p, size_t n) { static const char *d = "0123456789abcdef"; std::string s; for (size_t i = 0; i < n; ++i) { s += d[p[i] >> 4]; s += d[p[i] & 15]; } return s; } |
| 30 | std::string hex64(uint64_t v) { char b[17]; std::snprintf(b, sizeof(b), "%016llx", static_cast<unsigned long long>(v)); return b; } |
| 31 | std::vector<uint8_t> unhex(const std::string &s) { std::vector<uint8_t> o; for (size_t i = 0; i + 1 < s.size(); i += 2) o.push_back(static_cast<uint8_t>(std::stoi(s.substr(i, 2), nullptr, 16))); return o; } |
| 32 | |
| 33 | struct Input { std::string id, profile; uint32_t version = 0, level = 1, layout = 0; std::vector<uint8_t> payload; }; |
| 34 | using Result = std::map<std::string, std::string>; |
| 35 | |
| 36 | // Geometry hash: for each shape, kind, layer, paint, round_cap as int32 LE, |
| 37 | // then p[0..4] and width as int64 LE of round(value * 1e6). |
| 38 | uint64_t geometry_hash(const std::vector<ckey_shape> &shapes) { |
| 39 | uint64_t h = 0xcbf29ce484222325ull; |
| 40 | for (const auto &s : shapes) { |
| 41 | const int32_t ints[4] = {static_cast<int32_t>(s.kind), static_cast<int32_t>(s.layer), s.paint, static_cast<int32_t>(s.round_cap)}; |
| 42 | for (int32_t v : ints) { uint8_t b[4]; for (int i = 0; i < 4; ++i) b[i] = static_cast<uint8_t>(static_cast<uint32_t>(v) >> (8 * i)); h = fnv(b, 4, h); } |
| 43 | const double vals[6] = {s.p[0], s.p[1], s.p[2], s.p[3], s.p[4], s.width}; |
| 44 | for (double v : vals) { const int64_t q = std::llround(v * 1e6); uint8_t b[8]; for (int i = 0; i < 8; ++i) b[i] = static_cast<uint8_t>(static_cast<uint64_t>(q) >> (8 * i)); h = fnv(b, 8, h); } |
| 45 | } |
| 46 | return h; |
| 47 | } |
| 48 | |
| 49 | Result run(const Input &in) { |
| 50 | Result r; ckey_diagnostics d{}; |
| 51 | if (in.profile == "0") { |
| 52 | auto s = std::make_unique<ckey_scalable_symbol>(); |
| 53 | if (ckey_encode_scalable(in.payload.data(), in.payload.size(), in.version, static_cast<ckey_ecc_level>(in.level), static_cast<ckey_scalable_layout>(in.layout), s.get(), &d) != CKEY_OK) { r["error"] = d.message; return r; } |
| 54 | r["version"] = std::to_string(s->version); |
| 55 | r["codeword_fnv1a64"] = hex64(fnv(s->codeword, s->codeword_length)); |
| 56 | if (s->codeword_length <= 160) r["codeword_hex"] = hex(s->codeword, s->codeword_length); |
| 57 | size_t count = 0; ckey_scalable_geometry(s.get(), nullptr, nullptr, 0, &count); |
| 58 | std::vector<ckey_shape> shapes(count); ckey_scalable_geometry(s.get(), nullptr, shapes.data(), shapes.size(), &count); |
| 59 | r["shape_count"] = std::to_string(count); |
| 60 | r["geometry_fnv1a64"] = hex64(geometry_hash(shapes)); |
| 61 | size_t n = 0; ckey_write_interchange(s.get(), nullptr, nullptr, 0, &n); |
| 62 | std::vector<uint8_t> file(n); ckey_write_interchange(s.get(), nullptr, file.data(), n, &n); |
| 63 | r["ckey_file_fnv1a64"] = hex64(fnv(file.data(), n)); |
| 64 | // Behavioural: binary raster at 8 px/module decodes to the payload. |
| 65 | const uint32_t modules = ckey_scalable_modules_across(s->version); |
| 66 | ckey_raster_options o; ckey_raster_options_default(&o); |
| 67 | o.size_px = static_cast<uint32_t>(std::ceil(31.0 * 8 * modules / 29.0)); o.policy = CKEY_RASTER_BINARY; o.format = CKEY_PIXELS_GRAY8; |
| 68 | std::vector<uint8_t> px(static_cast<size_t>(o.size_px) * o.size_px); |
| 69 | bool ok = ckey_render_scalable_raster(s.get(), nullptr, &o, px.data(), px.size(), &n) == CKEY_OK; |
| 70 | const double k = o.size_px / 31.0, q[8] = {k, k, 30 * k, k, 30 * k, 30 * k, k, 30 * k}; |
| 71 | std::vector<uint8_t> out(65535); size_t len = 0; uint32_t v = 0, lay = 0; |
| 72 | ok = ok && ckey_decode_scalable_gray8(px.data(), o.size_px, o.size_px, o.size_px, q, out.data(), out.size(), &len, &v, &lay, &d) == CKEY_OK && |
| 73 | len == in.payload.size() && std::equal(in.payload.begin(), in.payload.end(), out.begin()) && v == s->version && lay == in.layout; |
| 74 | r["raster_decodes"] = ok ? "true" : "false"; |
| 75 | } else if (in.profile == "1") { |
| 76 | ckey_symbol s{}; if (ckey_encode_locator(in.payload.data(), in.payload.size(), &s, &d) != CKEY_OK) { r["error"] = d.message; return r; } |
| 77 | r["codeword_hex"] = hex(s.codeword, sizeof(s.codeword)); r["codeword_fnv1a64"] = hex64(fnv(s.codeword, sizeof(s.codeword))); |
| 78 | } else if (in.profile == "2") { |
| 79 | auto s = std::make_unique<ckey_text_symbol>(); if (ckey_encode_text(in.payload.data(), in.payload.size(), s.get(), &d) != CKEY_OK) { r["error"] = d.message; return r; } |
| 80 | r["codeword_fnv1a64"] = hex64(fnv(s->codeword, sizeof(s->codeword))); |
| 81 | } else if (in.profile == "3") { |
| 82 | auto s = std::make_unique<ckey_extended_symbol>(); if (ckey_encode_extended(in.payload.data(), in.payload.size(), s.get(), &d) != CKEY_OK) { r["error"] = d.message; return r; } |
| 83 | r["codeword_fnv1a64"] = hex64(fnv(s->codeword, sizeof(s->codeword))); |
| 84 | } |
| 85 | return r; |
| 86 | } |
| 87 | |
| 88 | std::vector<Input> default_inputs() { |
| 89 | std::vector<Input> v; |
| 90 | const auto bytes = [](size_t n, uint32_t seed) { std::vector<uint8_t> p(n); for (auto &b : p) { seed = seed * 1664525u + 1013904223u; b = static_cast<uint8_t>(seed >> 24); } return p; }; |
| 91 | const auto text = [](const std::string &s) { return std::vector<uint8_t>(s.begin(), s.end()); }; |
| 92 | v.push_back({"p0-auto-url", "0", 0, 1, 0, text("https://openckey.org/tour")}); |
| 93 | v.push_back({"p0-empty", "0", 1, 1, 0, {}}); |
| 94 | v.push_back({"p0-v1-L", "0", 1, 0, 0, bytes(30, 1)}); |
| 95 | for (uint32_t ver : {2u, 7u, 16u, 31u}) for (uint32_t level : {0u, 3u}) { |
| 96 | const size_t cap = ckey_scalable_capacity(ver, static_cast<ckey_ecc_level>(level), CKEY_LAYOUT_STANDARD); |
| 97 | v.push_back({"p0-v" + std::to_string(ver) + "-" + "LMQH"[level] + "-full", "0", ver, level, 0, bytes(cap, ver * 7 + level)}); |
| 98 | } |
| 99 | for (uint32_t ver : {1u, 3u, 12u, 31u}) { |
| 100 | const size_t cap = ckey_scalable_capacity(ver, CKEY_ECC_M, CKEY_LAYOUT_PRINT); |
| 101 | v.push_back({"p01-v" + std::to_string(ver) + "-M", "0", ver, 1, 1, bytes(std::min<size_t>(cap, 100), ver * 13)}); |
| 102 | } |
| 103 | v.push_back({"p0-vcard", "0", 0, 2, 0, text("BEGIN:VCARD\nVERSION:4.0\nFN:Ada Lovelace\nTEL:+15551234567\nEND:VCARD")}); |
| 104 | v.push_back({"p1-locator", "1", 0, 0, 0, bytes(16, 99)}); |
| 105 | v.push_back({"p2-text", "2", 0, 0, 0, text("CKey Profile 2 conformance vector")}); |
| 106 | v.push_back({"p3-extended", "3", 0, 0, 0, bytes(600, 5)}); |
| 107 | return v; |
| 108 | } |
| 109 | |
| 110 | // --- minimal JSON for this file's own shape (objects, arrays, strings, numbers) --- |
| 111 | struct J { enum T { Null, Num, Str, Arr, Obj } t = Null; double n = 0; std::string s; std::vector<J> a; std::vector<std::string> keys; std::vector<J> values; // object members (vector<J> allows the incomplete type) |
| 112 | const J *get(const std::string &k) const { for (size_t i = 0; i < keys.size(); ++i) if (keys[i] == k) return &values[i]; return nullptr; } }; |
| 113 | struct P { const std::string &s; size_t i = 0; |
| 114 | void ws() { while (i < s.size() && std::isspace(static_cast<unsigned char>(s[i]))) ++i; } |
| 115 | J parse() { ws(); J j; if (s[i] == '{') { ++i; j.t = J::Obj; ws(); if (s[i] == '}') { ++i; return j; } |
| 116 | for (;;) { ws(); J k = parse(); ws(); ++i; J v = parse(); j.keys.push_back(k.s); j.values.push_back(v); ws(); if (s[i++] == '}') break; } } |
| 117 | else if (s[i] == '[') { ++i; j.t = J::Arr; ws(); if (s[i] == ']') { ++i; return j; } for (;;) { j.a.push_back(parse()); ws(); if (s[i++] == ']') break; } } |
| 118 | else if (s[i] == '"') { ++i; j.t = J::Str; while (s[i] != '"') { if (s[i] == '\\') ++i; j.s += s[i++]; } ++i; } |
| 119 | else { j.t = J::Num; size_t e = i; while (e < s.size() && (std::isdigit(static_cast<unsigned char>(s[e])) || s[e] == '-' || s[e] == '.')) ++e; j.n = std::stod(s.substr(i, e - i)); i = e; } |
| 120 | return j; } }; |
| 121 | |
| 122 | } // namespace |
| 123 | |
| 124 | int main(int argc, char **argv) { |
| 125 | if (argc != 3 || (std::string(argv[1]) != "--write" && std::string(argv[1]) != "--check")) { std::cerr << "usage: ckey-conformance --write|--check <file.json>\n"; return 2; } |
| 126 | const std::string path = argv[2]; |
| 127 | if (std::string(argv[1]) == "--write") { |
| 128 | std::ofstream f(path); |
| 129 | f << "{\n \"suite\": \"ckey-conformance\",\n \"suite_version\": 1,\n \"generated_by\": \"" << ckey_get_software_version() << "\",\n" |
| 130 | << " \"hash\": \"FNV-1a 64; see tools/conformance/README.md\",\n \"vectors\": [\n"; |
| 131 | const auto inputs = default_inputs(); |
| 132 | for (size_t i = 0; i < inputs.size(); ++i) { |
| 133 | const auto &in = inputs[i]; const Result r = run(in); |
| 134 | f << " {\"id\": \"" << in.id << "\", \"profile\": \"" << in.profile << "\", \"version\": " << in.version << ", \"level\": " << in.level |
| 135 | << ", \"layout\": " << in.layout << ", \"payload_hex\": \"" << hex(in.payload.data(), in.payload.size()) << "\",\n \"expect\": {"; |
| 136 | size_t k = 0; for (const auto &[key, value] : r) f << (k++ ? ", " : "") << '"' << key << "\": \"" << value << '"'; |
| 137 | f << "}}" << (i + 1 < inputs.size() ? "," : "") << "\n"; |
| 138 | } |
| 139 | f << " ]\n}\n"; |
| 140 | std::cout << "wrote " << inputs.size() << " vectors to " << path << '\n'; |
| 141 | return 0; |
| 142 | } |
| 143 | std::ifstream f(path); std::stringstream ss; ss << f.rdbuf(); const std::string text = ss.str(); |
| 144 | if (text.empty()) { std::cerr << "cannot read " << path << '\n'; return 2; } |
| 145 | P parser{text}; const J root = parser.parse(); |
| 146 | size_t passed = 0, failed = 0; |
| 147 | for (const J &v : root.get("vectors")->a) { |
| 148 | Input in; in.id = v.get("id")->s; in.profile = v.get("profile")->s; |
| 149 | in.version = static_cast<uint32_t>(v.get("version")->n); in.level = static_cast<uint32_t>(v.get("level")->n); in.layout = static_cast<uint32_t>(v.get("layout")->n); |
| 150 | in.payload = unhex(v.get("payload_hex")->s); |
| 151 | const Result got = run(in); |
| 152 | bool ok = true; |
| 153 | const J &expect = *v.get("expect"); |
| 154 | for (size_t e = 0; e < expect.keys.size(); ++e) { |
| 155 | const std::string &key = expect.keys[e]; const J &want = expect.values[e]; |
| 156 | const auto it = got.find(key); |
| 157 | if (it == got.end() || it->second != want.s) { std::cerr << in.id << ": " << key << " expected " << want.s << " got " << (it == got.end() ? "(missing)" : it->second) << '\n'; ok = false; } |
| 158 | } |
| 159 | (ok ? passed : failed)++; |
| 160 | } |
| 161 | std::cout << "conformance: " << passed << " passed, " << failed << " failed\n"; |
| 162 | return failed ? 1 : 0; |
| 163 | } |