Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
api_interchange.inc
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | // Public C ABI: .ckey interchange (spec/CKey-Interchange-v1.md). Part of |
| 2 | // ckey_codec: it needs only framing and the codeword decoder. |
| 3 | |
| 4 | namespace { |
| 5 | |
| 6 | // Record types. Bit 15 set = optional (a reader may skip it if unknown). |
| 7 | constexpr uint16_t kRecordCodeword = 0x0001, kRecordStyle = 0x8002, kRecordLogo = 0x8003; |
| 8 | constexpr size_t kInterchangeHeader = 12, kStyleRecordBytes = 64; |
| 9 | |
| 10 | void put16(std::vector<uint8_t> &o, uint16_t v) { o.push_back(static_cast<uint8_t>(v)); o.push_back(static_cast<uint8_t>(v >> 8)); } |
| 11 | void put32(std::vector<uint8_t> &o, uint32_t v) { for (int s = 0; s < 32; s += 8) o.push_back(static_cast<uint8_t>(v >> s)); } |
| 12 | void put_unit(std::vector<uint8_t> &o, double v) { put16(o, static_cast<uint16_t>(std::lround(std::clamp(v, 0.0, 1.0) * 65535))); } |
| 13 | uint16_t get16(const uint8_t *p) { return static_cast<uint16_t>(p[0] | (p[1] << 8)); } |
| 14 | uint32_t get32(const uint8_t *p) { return static_cast<uint32_t>(p[0]) | (static_cast<uint32_t>(p[1]) << 8) | (static_cast<uint32_t>(p[2]) << 16) | (static_cast<uint32_t>(p[3]) << 24); } |
| 15 | |
| 16 | // Fixed 64-byte style record: colours, shapes, effects (16-bit fixed point), |
| 17 | // logo radius (1/1000 unit) and flags. Logos travel as their own record. |
| 18 | std::vector<uint8_t> encode_style(const ckey_style &st) { |
| 19 | std::vector<uint8_t> o; |
| 20 | for (const ckey_rgb &c : {st.background, st.background_edge, st.ink, st.ink_shade, st.accents[0], st.accents[1], st.accents[2], st.accents[3]}) { o.push_back(c.r); o.push_back(c.g); o.push_back(c.b); } |
| 21 | o.push_back(static_cast<uint8_t>(st.accent_count)); o.push_back(static_cast<uint8_t>(st.accent_percent)); |
| 22 | o.push_back(static_cast<uint8_t>(st.modules)); o.push_back(static_cast<uint8_t>(st.rings)); o.push_back(static_cast<uint8_t>(st.rays)); |
| 23 | o.push_back(static_cast<uint8_t>(st.policy)); o.push_back(st.logo_plate ? 1 : 0); |
| 24 | put_unit(o, st.bevel); put_unit(o, st.shadow); put_unit(o, st.texture); |
| 25 | put32(o, static_cast<uint32_t>(std::lround(std::clamp(st.logo_radius, 0.0, 1e6) * 1000))); |
| 26 | while (o.size() < kStyleRecordBytes) o.push_back(0); |
| 27 | return o; |
| 28 | } |
| 29 | |
| 30 | bool decode_style(const uint8_t *p, ckey_style &st) { |
| 31 | fill_default_style(st); |
| 32 | ckey_rgb *colours[8] = {&st.background, &st.background_edge, &st.ink, &st.ink_shade, &st.accents[0], &st.accents[1], &st.accents[2], &st.accents[3]}; |
| 33 | for (int i = 0; i < 8; ++i) *colours[i] = {p[3 * i], p[3 * i + 1], p[3 * i + 2]}; |
| 34 | const uint8_t *q = p + 24; |
| 35 | st.accent_count = q[0]; st.accent_percent = q[1]; |
| 36 | st.modules = static_cast<ckey_module_shape>(q[2]); st.rings = static_cast<ckey_ring_style>(q[3]); st.rays = static_cast<ckey_ray_cap>(q[4]); |
| 37 | st.policy = q[5]; st.logo_plate = q[6]; |
| 38 | st.bevel = get16(q + 7) / 65535.0; st.shadow = get16(q + 9) / 65535.0; st.texture = get16(q + 11) / 65535.0; |
| 39 | st.logo_radius = get32(q + 13) / 1000.0; |
| 40 | for (size_t i = 24 + 17; i < kStyleRecordBytes; ++i) if (p[i]) return false; // reserved bytes must be zero |
| 41 | return st.accent_count <= 4 && st.accent_percent <= 100 && q[2] <= CKEY_MODULE_DIAMOND && q[3] <= CKEY_RING_DOTS && |
| 42 | q[4] <= CKEY_RAY_FLAT && q[5] <= CKEY_POLICY_OVERRIDE && q[6] <= 1 && st.logo_radius <= 14.5; |
| 43 | } |
| 44 | |
| 45 | } // namespace |
| 46 | |
| 47 | extern "C" { |
| 48 | |
| 49 | ckey_status ckey_write_interchange(const ckey_scalable_symbol *s, const ckey_style *style, uint8_t *out, size_t cap, size_t *required) { |
| 50 | if (!s || !required || s->abi_version != CKEY_ABI_VERSION || s->version < 1 || s->version > CKEY_SCALABLE_VERSION_COUNT || |
| 51 | s->ecc_level > 3 || s->layout >= static_cast<uint32_t>(kScalableFamilies)) return CKEY_INVALID_ARGUMENT; |
| 52 | const ScalableLayout &L = scalable_layout(static_cast<int>(s->version), static_cast<int>(s->layout)); |
| 53 | if (s->codeword_length != L.codeword_bytes) return CKEY_INVALID_ARGUMENT; |
| 54 | std::vector<uint8_t> o = {'C', 'K', 'E', 'Y', static_cast<uint8_t>(CKEY_INTERCHANGE_VERSION), 0, 0, 0, |
| 55 | L.format, static_cast<uint8_t>(s->version), static_cast<uint8_t>(s->ecc_level), static_cast<uint8_t>(s->layout)}; |
| 56 | put16(o, kRecordCodeword); put32(o, static_cast<uint32_t>(s->codeword_length)); |
| 57 | o.insert(o.end(), s->codeword, s->codeword + s->codeword_length); |
| 58 | if (style) { |
| 59 | if (!structural_problem(L, *style).empty()) return CKEY_INVALID_ARGUMENT; |
| 60 | const auto rec = encode_style(*style); |
| 61 | put16(o, kRecordStyle); put32(o, static_cast<uint32_t>(rec.size())); o.insert(o.end(), rec.begin(), rec.end()); |
| 62 | if (style->logo_href && *style->logo_href) { |
| 63 | const size_t n = std::strlen(style->logo_href); |
| 64 | if (n > 32768) return CKEY_INVALID_ARGUMENT; |
| 65 | put16(o, kRecordLogo); put32(o, static_cast<uint32_t>(n)); o.insert(o.end(), style->logo_href, style->logo_href + n); |
| 66 | } |
| 67 | } |
| 68 | put32(o, crc32c(o.data(), o.size())); |
| 69 | if (o.size() > CKEY_INTERCHANGE_MAX_BYTES) return CKEY_INVALID_ARGUMENT; |
| 70 | *required = o.size(); |
| 71 | if (!out || cap < o.size()) return CKEY_INSUFFICIENT_BUFFER; |
| 72 | std::memcpy(out, o.data(), o.size()); |
| 73 | return CKEY_OK; |
| 74 | } |
| 75 | |
| 76 | ckey_status ckey_read_interchange(const uint8_t *data, size_t len, ckey_scalable_symbol *sym, ckey_style *out_style, |
| 77 | char *logo_href, size_t logo_cap, ckey_diagnostics *diag) { |
| 78 | const auto fail = [&](const char *why) { set_status(diag, CKEY_INVALID_ARGUMENT, why); return CKEY_INVALID_ARGUMENT; }; |
| 79 | if (!data || !sym) return fail(".ckey reader needs data and a symbol"); |
| 80 | if (len < kInterchangeHeader + 6 + 4 || len > CKEY_INTERCHANGE_MAX_BYTES) return fail(".ckey file is too short or too large"); |
| 81 | if (std::memcmp(data, "CKEY", 4) != 0) return fail("not a .ckey file"); |
| 82 | if (data[4] != CKEY_INTERCHANGE_VERSION) return fail("unsupported .ckey version"); |
| 83 | if (data[5] || data[6] || data[7]) return fail(".ckey reserved header bytes must be zero"); |
| 84 | if (crc32c(data, len - 4) != get32(data + len - 4)) return fail(".ckey CRC-32C mismatch"); |
| 85 | const uint8_t format = data[8], version = data[9], level = data[10], layout = data[11]; |
| 86 | if (version < 1 || version > CKEY_SCALABLE_VERSION_COUNT || level > 3 || layout >= kScalableFamilies) return fail(".ckey symbol header out of range"); |
| 87 | const ScalableLayout &L = scalable_layout(version, layout); |
| 88 | if (format != L.format) return fail(".ckey format byte does not match its layout"); |
| 89 | bool have_codeword = false, have_style = false; |
| 90 | ckey_scalable_symbol s{}; s.abi_version = CKEY_ABI_VERSION; s.version = version; s.ecc_level = level; s.layout = layout; |
| 91 | ckey_style st{}; std::string logo; |
| 92 | size_t at = kInterchangeHeader; const size_t end = len - 4; |
| 93 | while (at < end) { |
| 94 | if (end - at < 6) return fail(".ckey record header truncated"); |
| 95 | const uint16_t type = get16(data + at); const uint32_t n = get32(data + at + 2); at += 6; |
| 96 | if (n > end - at) return fail(".ckey record overruns the file"); |
| 97 | const uint8_t *v = data + at; |
| 98 | if (type == kRecordCodeword) { |
| 99 | if (have_codeword || n != L.codeword_bytes) return fail(".ckey codeword record is duplicated or has the wrong length"); |
| 100 | std::memcpy(s.codeword, v, n); s.codeword_length = n; have_codeword = true; |
| 101 | } else if (type == kRecordStyle) { |
| 102 | if (have_style || n != kStyleRecordBytes || !decode_style(v, st)) return fail(".ckey style record is invalid"); |
| 103 | have_style = true; |
| 104 | } else if (type == kRecordLogo) { |
| 105 | if (!have_style || !logo.empty() || n == 0 || n > 32768) return fail(".ckey logo record is invalid"); |
| 106 | for (uint32_t i = 0; i < n; ++i) if (v[i] < 0x20 || v[i] == 0x7f) return fail(".ckey logo reference has control characters"); |
| 107 | logo.assign(reinterpret_cast<const char *>(v), n); |
| 108 | } else if (!(type & 0x8000u)) return fail(".ckey has an unknown required record"); |
| 109 | at += n; |
| 110 | } |
| 111 | if (!have_codeword) return fail(".ckey has no codeword"); |
| 112 | // The codeword must decode: a .ckey never carries a symbol a reader would reject. |
| 113 | std::vector<uint8_t> payload(65535); size_t payload_len = 0; ckey_diagnostics d{}; |
| 114 | if (ckey_decode_scalable(s.codeword, s.codeword_length, version, static_cast<ckey_scalable_layout>(layout), payload.data(), payload.size(), &payload_len, &d) != CKEY_OK) |
| 115 | return fail(".ckey codeword does not decode"); |
| 116 | // Same check as the encoder: the stored codeword must be the canonical, |
| 117 | // error-free encoding (a corrected codeword would render differently). |
| 118 | ckey_scalable_symbol again{}; ckey_diagnostics e{}; |
| 119 | if (ckey_encode_scalable(payload.data(), payload_len, version, static_cast<ckey_ecc_level>(level), static_cast<ckey_scalable_layout>(layout), &again, &e) != CKEY_OK || |
| 120 | again.codeword_length != s.codeword_length || std::memcmp(again.codeword, s.codeword, s.codeword_length) != 0) |
| 121 | return fail(".ckey codeword is not a canonical encoding"); |
| 122 | *sym = s; |
| 123 | if (out_style) { |
| 124 | if (have_style) { |
| 125 | st.logo_href = nullptr; |
| 126 | if (!logo.empty() && logo_href && logo_cap > logo.size()) { std::memcpy(logo_href, logo.c_str(), logo.size() + 1); st.logo_href = logo_href; } |
| 127 | *out_style = st; |
| 128 | } else fill_default_style(*out_style); |
| 129 | } |
| 130 | set_status(diag, CKEY_OK, have_style ? "read .ckey symbol and style" : "read .ckey symbol"); |
| 131 | return CKEY_OK; |
| 132 | } |
| 133 | |
| 134 | } // extern "C" |