# distutils: language = c++ # cython: language_level=3 import math import json import hashlib import sys from libcpp.string cimport string from cpython.bytes cimport PyBytes_FromStringAndSize, PyBytes_AS_STRING, PyBytes_GET_SIZE from cpython.unicode cimport PyUnicode_GET_LENGTH, PyUnicode_READ_CHAR FALLBACK = object() cdef Py_ssize_t LIMIT = 128 * 1024 * 1024 cdef int DEPTH = 32 cdef bint budget(): cdef int frames = 0 cdef object frame if sys.getrecursionlimit() < 512: return False frame = sys._getframe() while frame is not None and frames <= 64: frames += 1 frame = frame.f_back return frames <= 64 cdef object limits(object item, int depth, dict memo, set active): cdef object kind = type(item), ident = id(item), cached, child, key, row, value cdef Py_ssize_t cost = 0, child_cost cdef int height = 0, maximum = -1 if depth > DEPTH or ident in active: return FALLBACK cached = memo.get(ident) if cached is not None: return cached if depth + cached[1] <= DEPTH else FALLBACK if item is None: cost = 1 elif kind is bool: cost = 2 elif kind is int: value = 9 + (abs(item).bit_length() + 7) // 8 if value > LIMIT: return FALLBACK cost = value elif kind is float: if not math.isfinite(item): return FALLBACK cost = 9 elif kind is str or kind is bytes: value = 9 + len(item.encode('utf8', 'surrogatepass') if kind is str else item) if value > LIMIT: return FALLBACK cost = value elif kind is tuple or kind is list or kind is dict: if kind is dict: for key in item: if type(key) is not str: return FALLBACK active.add(ident) cost = 9 try: if kind is dict: for key, child in item.items(): for value in (key, child): row = limits(value, depth + 1, memo, active) if row is FALLBACK: return FALLBACK child_cost = row[0] if child_cost > LIMIT - cost: return FALLBACK cost += child_cost maximum = max(maximum, row[1]) else: for child in item: row = limits(child, depth + 1, memo, active) if row is FALLBACK: return FALLBACK child_cost = row[0] if child_cost > LIMIT - cost: return FALLBACK cost += child_cost maximum = max(maximum, row[1]) finally: active.remove(ident) height = 1 + maximum else: return FALLBACK if depth + height > DEPTH or cost > LIMIT: return FALLBACK row = (cost, height) memo[ident] = row return row cdef object clone(object item): cdef object kind = type(item), child, key cdef dict mapping cdef list rows if kind is dict: mapping = {} for key, child in item.items(): mapping[key] = clone(child) return mapping if kind is tuple or kind is list: rows = [] for child in item: rows.append(clone(child)) return tuple(rows) if kind is tuple else rows return item cdef object typed(object item): cdef object kind = type(item), child, key cdef list rows = [] if item is None or kind is bool or kind is int or kind is str: return [kind.__name__, item] if kind is float: return ['float64', item.hex()] if kind is bytes: return ['bytes', item.hex()] if kind is tuple or kind is list: for child in item: rows.append(typed(child)) return [kind.__name__, rows] for key in sorted(item): rows.append([key, typed(item[key])]) return ['dict', rows] def independent_primitive_graph(object value): if not budget() or limits(value, 0, {}, set()) is FALLBACK: return FALLBACK return clone(value) cdef object shape(object item, int depth, dict memo, set active): cdef object kind = type(item), ident, cached, child, key, height cdef int maximum = -1 if depth > DEPTH: return FALLBACK if item is None or kind is bool or kind is int or kind is str or kind is bytes: return 0 if kind is float: return 0 if math.isfinite(item) else FALLBACK if kind is not tuple and kind is not list and kind is not dict: return FALLBACK ident = id(item) if ident in active: return FALLBACK cached = memo.get(ident) if cached is not None: return cached if depth + cached <= DEPTH else FALLBACK if kind is dict: for key in item: if type(key) is not str: return FALLBACK active.add(ident) try: for child in (item.values() if kind is dict else item): height = shape(child, depth + 1, memo, active) if height is FALLBACK: return FALLBACK maximum = max(maximum, height) finally: active.remove(ident) height = 1 + maximum if depth + height > DEPTH: return FALLBACK memo[ident] = height return height cdef bint large(object value, int depth): cdef object kind = type(value), child if kind is tuple or kind is list: return len(value) > 256 if kind is dict and depth < 3: for child in value.values(): if large(child, depth + 1): return True return False cdef void update_digest(object digest, object value): cdef object kind = type(value), child, key, text cdef list batch cdef Py_ssize_t start, index = 0 if (kind is tuple or kind is list) and len(value) > 256: digest.update(b'["tuple",[' if kind is tuple else b'["list",[') for start in range(0, len(value), 64): if start: digest.update(b',') batch = [] for child in value[start:start + 64]: batch.append(typed(child)) text = json.dumps(batch, separators=(',', ':'), allow_nan=False) digest.update(text[1:-1].encode()) digest.update(b']]') elif kind is dict and large(value, 0): digest.update(b'["dict",[') for key in sorted(value): if index: digest.update(b',') digest.update(('[' + json.dumps(key, separators=(',', ':'), allow_nan=False) + ',').encode()) update_digest(digest, value[key]) digest.update(b']') index += 1 digest.update(b']]') else: digest.update(json.dumps(typed(value), separators=(',', ':'), allow_nan=False).encode()) # One fixed-capacity native buffer, no Python token lists or typed-tree copy. # Every byte is ASCII JSON. Admission remains the original full shape prepass. cdef class JsonBuffer: cdef string data cdef object digest def __cinit__(self, object digest): self.digest = digest self.data.reserve(65536) cdef void flush(self) except *: if self.data.size(): self.digest.update(PyBytes_FromStringAndSize(self.data.data(), self.data.size())) self.data.clear() cdef void put(self, char value) except *: self.data.push_back(value) if self.data.size() == 65536: self.flush() cdef void literal(self, const char* value, Py_ssize_t size) except *: cdef Py_ssize_t offset = 0, available, take while offset < size: available = 65536 - self.data.size() take = min(available, size - offset) self.data.append(value + offset, take) offset += take if self.data.size() == 65536: self.flush() cdef void unicode_json(JsonBuffer output, object value) except *: cdef Py_ssize_t index, length cdef bytes encoded cdef unsigned int code, high, low cdef const char* digits = "0123456789abcdef" # A digest flush can release the GIL or reenter Python after shape(). A later # dict key may therefore no longer be an exact str. The old typed-tree JSON # path encodes such a raw key as a JSON value; preserve that safe behavior. if type(value) is not str: encoded = json.dumps(value, separators=(',', ':'), allow_nan=False).encode() output.literal(PyBytes_AS_STRING(encoded), PyBytes_GET_SIZE(encoded)) return length = PyUnicode_GET_LENGTH(value) output.put(34) for index in range(length): code = PyUnicode_READ_CHAR(value, index) if code == 34 or code == 92: output.put(92) output.put(code) elif code == 8: output.literal('\\b', 2) elif code == 9: output.literal('\\t', 2) elif code == 10: output.literal('\\n', 2) elif code == 12: output.literal('\\f', 2) elif code == 13: output.literal('\\r', 2) elif 32 <= code <= 126: output.put(code) else: if code > 65535: high = 55296 + ((code - 65536) >> 10) low = 56320 + ((code - 65536) & 1023) else: high = code low = 0 output.literal('\\u', 2) output.put(digits[(high >> 12) & 15]) output.put(digits[(high >> 8) & 15]) output.put(digits[(high >> 4) & 15]) output.put(digits[high & 15]) if code > 65535: output.literal('\\u', 2) output.put(digits[(low >> 12) & 15]) output.put(digits[(low >> 8) & 15]) output.put(digits[(low >> 4) & 15]) output.put(digits[low & 15]) output.put(34) cdef void buffer_typed(JsonBuffer output, object value) except *: cdef object kind = type(value), child, key cdef bytes encoded cdef const unsigned char* raw cdef const char* digits = "0123456789abcdef" cdef Py_ssize_t index = 0, length, offset, take, byte_index cdef char hex_chunk[8192] if value is None: output.literal('["NoneType",null]', 17) elif kind is bool: if value: output.literal('["bool",true]', 13) else: output.literal('["bool",false]', 14) elif kind is int: output.literal('["int",', 7) # Exact int repr is also the C JSON encoder's decimal spelling and keeps # CPython's configured digit-limit exceptions. No subclass method can run. encoded = str(value).encode('ascii') output.literal(PyBytes_AS_STRING(encoded), PyBytes_GET_SIZE(encoded)) output.put(93) elif kind is float: output.literal('["float64",', 11) unicode_json(output, value.hex()) output.put(93) elif kind is str: output.literal('["str",', 7) unicode_json(output, value) output.put(93) elif kind is bytes: output.literal('["bytes","', 10) raw = PyBytes_AS_STRING(value) length = PyBytes_GET_SIZE(value) for offset in range(0, length, 4096): take = min(4096, length - offset) for byte_index in range(take): hex_chunk[2 * byte_index] = digits[raw[offset + byte_index] >> 4] hex_chunk[2 * byte_index + 1] = digits[raw[offset + byte_index] & 15] output.literal(hex_chunk, 2 * take) output.literal('"]', 2) elif kind is tuple or kind is list: if kind is tuple: output.literal('["tuple",[', 10) else: output.literal('["list",[', 9) for child in value: if index: output.put(44) buffer_typed(output, child) index += 1 output.literal(']]', 2) else: output.literal('["dict",[', 9) for key in sorted(value): if index: output.put(44) output.put(91) unicode_json(output, key) output.put(44) buffer_typed(output, value[key]) output.put(93) index += 1 output.literal(']]', 2) def whole_body_digest(object value): cdef JsonBuffer output if not budget() or shape(value, 0, {}, set()) is FALLBACK: return FALLBACK digest = hashlib.sha256() if large(value, 0): output = JsonBuffer(digest) buffer_typed(output, value) output.flush() else: update_digest(digest, value) return digest.hexdigest() cdef extern from "Python.h": int PY_MAJOR_VERSION int PY_MINOR_VERSION def compiled_abi(): return (PY_MAJOR_VERSION, PY_MINOR_VERSION, sizeof(void*) * 8, 'flm-exact-primitives-v1')