XdrJsonHelper
in package
Primitive SEP-51 (XDR-JSON) helper methods.
This class provides low-level encoding and decoding utilities for the SEP-0051 standard mapping between Stellar XDR structures and their JSON representations. Higher-level toJson/fromJson methods on individual XDR classes delegate to these primitives for the byte-level and numeric operations defined in the spec.
All methods are stateless and operate only on their arguments.
Tags
Table of Contents
Methods
- bytesToHex() : string
- SEP-51 Opaque type: encode raw bytes as a lowercase hex string.
- canonicalJson() : string
- Canonical JSON normalisation for structural JSON comparison.
- decodeText() : mixed
- Decode a SEP-0051 JSON document from its text form.
- escapeString() : string
- SEP-51 String type: escape bytes per the SEP-51 String escape ladder.
- hexToBytes() : string
- Inverse of bytesToHex: decode a lowercase hex string back to raw bytes.
- int128PartsToString() : string
- Assemble a signed 128-bit integer from hi/lo limbs and return as a base-10 string.
- int256PartsToString() : string
- Assemble a signed 256-bit integer from four limbs and return as a base-10 string.
- int64ToString() : string
- 64-bit signed integer to base-10 string (SEP-51 Hyper Integer encoding).
- ksortRecursive() : mixed
- Recursively sort object property names for canonical JSON normalisation.
- normalizeFieldAlias() : array<string|int, mixed>
- Fold a field's input alias into the field's canonical key.
- rejectDuplicateKeys() : void
- Reject a JSON document in which any single object repeats a key.
- rejectUnknownFields() : void
- Reject a struct-object JSON input that carries keys the type does not declare.
- safePreview() : string
- Return a safe, bounded preview of user-supplied input for use in exception messages.
- stringToInt128Parts() : array{hi: string, lo: string}
- Decompose a signed 128-bit integer string into hi/lo limbs.
- stringToInt256Parts() : array{hiHi: string, hiLo: string, loHi: string, loLo: string}
- Decompose a signed 256-bit integer string into four limbs.
- stringToInt64() : int
- Parse a base-10 integer string (or a PHP int) to a 64-bit signed int.
- stringToUint128Parts() : array{hi: string, lo: string}
- Decompose an unsigned 128-bit integer string into hi/lo uint64 parts.
- stringToUint256Parts() : array{hiHi: string, hiLo: string, loHi: string, loLo: string}
- Decompose an unsigned 256-bit integer string into four uint64 limbs.
- stringToUint64() : int
- Parse a base-10 unsigned-integer string (or a PHP int) to a uint64 value.
- uint128PartsToString() : string
- Assemble an unsigned 128-bit integer from hi/lo uint64 parts and return as a base-10 string.
- uint256PartsToString() : string
- Assemble an unsigned 256-bit integer from four uint64 limbs and return as a base-10 string.
- uint64ToString() : string
- 64-bit unsigned integer to base-10 string (SEP-51 Unsigned Hyper Integer encoding).
- unescapeString() : string
- Inverse of escapeString: decode a SEP-51 escaped-ASCII string back to raw bytes.
- wrapUnsignedToSignedInt() : int
- Wrap an unsigned uint64 base-10 string into the matching PHP signed-int representation used by the Parts struct fields.
Methods
bytesToHex()
SEP-51 Opaque type: encode raw bytes as a lowercase hex string.
public
static bytesToHex(string $bytes) : string
Per SEP-0051 the Opaque types (both fixed-length and variable-length) are represented as hexadecimal strings. An empty byte sequence encodes as an empty string "" (not "0" and not "00").
Parameters
- $bytes : string
-
Raw binary input (any length, including zero).
Return values
string —Lowercase hex string (even length; two hex chars per byte).
canonicalJson()
Canonical JSON normalisation for structural JSON comparison.
public
static canonicalJson(string $json) : string
The algorithm:
- Decode with assoc=false (stdClass mode) so the empty-object / empty-array distinction is preserved: json_decode('}') -> stdClass, json_decode('[]') -> [].
- Recurse via ksortRecursive: sort object property names lexicographically; preserve list order for indexed arrays.
- Re-encode with JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE so that forward slashes and non-ASCII codepoints are not double-escaped.
The result is a deterministic, whitespace-free JSON string. Two JSON values that are semantically equal (same structure, same key order after sorting, same values) will produce byte-identical canonical forms.
Note: this method does not impose an input-size limit. Callers are responsible for bounding $json length before invoking this method; an HTTP layer or file reader should apply a size limit appropriate to its threat model.
Parameters
- $json : string
-
Any valid JSON string.
Tags
Return values
string —Canonical (sorted-keys, compact) JSON string.
decodeText()
Decode a SEP-0051 JSON document from its text form.
public
static decodeText(string $json) : mixed
Every fromJson(string $json) entry point routes through here, so the
text-level input rules hold uniformly across all XDR types: the document is
scanned for duplicate object keys and then parsed.
The duplicate-key scan has to happen on the text because json_decode cannot report duplicates — it resolves them last-value-wins, which would silently discard the earlier value from a document that is not valid SEP-0051 input. See self::rejectDuplicateKeys.
The tree entry points (fromJsonValue, taking an already-decoded PHP value)
need no such check: a PHP array cannot hold two identical string keys, so a
duplicate is unrepresentable by the time a value reaches them.
Parameters
- $json : string
-
The raw JSON document.
Tags
Return values
mixed —The decoded value, with objects as associative arrays.
escapeString()
SEP-51 String type: escape bytes per the SEP-51 String escape ladder.
public
static escapeString(string $bytes) : string
Escaping rules (applied byte-by-byte on the raw binary input):
- 0x00 (NUL) -> \0
- 0x09 (TAB) -> \t
- 0x0A (LF) -> \n
- 0x0D (CR) -> \r
- 0x5C (BS) -> \
- 0x20..0x7E (printable ASCII, excluding backslash) -> verbatim
- all other bytes (0x01..0x08, 0x0B, 0x0C, 0x0E..0x1F, 0x7F, 0x80..0xFF) -> \xNN (lowercase hex, exactly two digits)
The input is treated as a raw byte string (not UTF-8); strlen() and ord() are used throughout — never mb_strlen() or mb_substr().
When this output is subsequently stored inside a JSON string literal the JSON encoder will escape each backslash a second time, producing the double-escaped form the spec specifies (e.g. "\xc3" in JSON text).
Parameters
- $bytes : string
-
Raw binary input (any byte sequence).
Return values
string —Escaped ASCII output (only printable ASCII bytes).
hexToBytes()
Inverse of bytesToHex: decode a lowercase hex string back to raw bytes.
public
static hexToBytes(string $hex) : string
Strict mode is enforced: only lowercase hexadecimal characters [0-9a-f] are accepted. Uppercase hex is rejected; SEP-0051 §Opaque specifies lowercase hex output, and the decoder mirrors that constraint so that round-trips remain canonical.
Parameters
- $hex : string
-
Lowercase hex string (even length; zero length allowed).
Tags
Return values
string —Raw binary bytes.
int128PartsToString()
Assemble a signed 128-bit integer from hi/lo limbs and return as a base-10 string.
public
static int128PartsToString(string $hi, string $lo) : string
Per XDR Int128Parts (verified against XdrInt128Parts.php encode path): hi is signed int64; lo is unsigned uint64.
result = hi_signed * 2^64 + lo_unsigned
lo_unsigned is obtained by treating the PHP int stored in $lo as an unsigned 64-bit value (i.e. adding 2^64 if negative). GMP is used throughout to handle the full range.
Parameters
- $hi : string
-
Signed int64 as base-10 string (range [-2^63, 2^63-1]).
- $lo : string
-
Unsigned uint64 as base-10 string. May be a negative base-10 string when the uint64 value exceeds PHP_INT_MAX and is stored as a signed PHP int (the negative form is reinterpreted as the unsigned bit pattern via two's-complement).
Tags
Return values
string —Base-10 decimal string of the assembled 128-bit signed integer.
int256PartsToString()
Assemble a signed 256-bit integer from four limbs and return as a base-10 string.
public
static int256PartsToString(string $hiHi, string $hiLo, string $loHi, string $loLo) : string
Per XDR Int256Parts (verified against XdrInt256Parts.php encode path):
- hiHi: signed int64 (most significant)
- hiLo: unsigned uint64
- loHi: unsigned uint64
- loLo: unsigned uint64 (least significant)
All limb strings may be supplied in either unsigned form ("18446744073709551615") or in PHP's signed two's-complement wrap form ("-1") — the internal uint64HalfToGmp helper handles both representations for lossless assembly.
Parameters
- $hiHi : string
-
Signed int64 as base-10 string (range [-2^63, 2^63-1]).
- $hiLo : string
-
Unsigned uint64 as base-10 string. May be a negative base-10 string when the uint64 value exceeds PHP_INT_MAX and is stored as a signed PHP int (the negative form is reinterpreted as the unsigned bit pattern via two's-complement).
- $loHi : string
-
Unsigned uint64 as base-10 string (same note as hiLo).
- $loLo : string
-
Unsigned uint64 as base-10 string (same note as hiLo).
Tags
Return values
string —Base-10 decimal string of the assembled 256-bit signed integer.
int64ToString()
64-bit signed integer to base-10 string (SEP-51 Hyper Integer encoding).
public
static int64ToString(int $value) : string
Parameters
- $value : int
-
A PHP int (64-bit signed on 64-bit systems).
Return values
string —Base-10 decimal string representation.
ksortRecursive()
Recursively sort object property names for canonical JSON normalisation.
public
static ksortRecursive(mixed $value[, int $depth = 512 ]) : mixed
Walk rules:
- stdClass (object): cast to array, ksort by string key, recurse into values, cast back to object. This preserves the } vs [] distinction in re-encoding.
- Indexed array (list-shape): preserve element order; recurse into elements.
- Associative array: ksort and recurse (defensive; should not occur in stdClass-mode decode but covered for callers using this method directly on mixed structures).
- Scalar / null: return as-is.
The $depth parameter bounds recursion depth, matching json_decode's own default of 512. Callers constructing deeply-nested PHP arrays by hand and passing them directly here (rather than via canonicalJson) should pass an appropriate depth bound. canonicalJson starts the recursion at depth=512 which aligns with json_decode's own limit.
Parameters
- $value : mixed
-
Any decoded JSON value.
- $depth : int = 512
-
Remaining recursion depth. Throws when it reaches zero.
Tags
Return values
mixed —The same structure with all object property keys sorted recursively.
normalizeFieldAlias()
Fold a field's input alias into the field's canonical key.
public
static normalizeFieldAlias(array<string|int, mixed> $value, string $canonicalKey, string $aliasKey, string $typeName) : array<string|int, mixed>
A struct field may accept a second spelling of its key on input while always emitting the canonical one. Rewriting the alias to the canonical key before any other check means the declared-key closure, the required-field check and the field read all work from the one spelling the type emits.
The two spellings name one field, so supplying both is a duplicate rather than two values: it is rejected instead of one silently winning.
Parameters
- $value : array<string|int, mixed>
-
The decoded JSON object.
- $canonicalKey : string
-
The key the type emits.
- $aliasKey : string
-
The additional key accepted on input.
- $typeName : string
-
The PHP class the object is being decoded into.
Tags
Return values
array<string|int, mixed> —$value with any alias entry moved to $canonicalKey.
rejectDuplicateKeys()
Reject a JSON document in which any single object repeats a key.
public
static rejectDuplicateKeys(string $json) : void
A repeated key has no SEP-0051 meaning: the document names one field twice with two values, and no rule says which one the type takes. PHP's json_decode silently keeps the last, so without this scan the first value would vanish from a document that is not valid input in the first place.
Scope is per object. The same key in sibling objects, or in an object nested inside another that already uses that key, is ordinary JSON and is accepted; only a repeat within one set of braces is a duplicate.
Keys are compared after their JSON escapes are resolved, so "a" and
"a" are the same key. Escape resolution produces UTF-8 bytes, which
makes "é" and a literal é the same key as well.
The walk is a single iterative pass over the raw bytes, with an explicit frame stack rather than recursion, so nesting depth cannot exhaust the PHP call stack. String tokens are consumed whole, so braces, commas and colons inside string content never disturb the structure being tracked.
This is a structural scan, not a JSON validator. On a structural fault it cannot navigate past — an unbalanced brace, an unterminated string, an unresolvable escape, nesting beyond self::DECODE_MAX_DEPTH — it stops and returns, leaving json_decode to report the syntax error. It does not inspect scalar values, so a document that is both duplicate-keyed and otherwise malformed may surface either error depending on which fault the scan reaches first; both outcomes reject the document.
Parameters
- $json : string
-
The raw JSON document.
Tags
rejectUnknownFields()
Reject a struct-object JSON input that carries keys the type does not declare.
public
static rejectUnknownFields(array<string|int, mixed> $value, array<int, string> $declaredKeys, string $typeName) : void
SEP-0051 defines the JSON encoding of a struct as an object whose keys are exactly the struct's fields. A key outside that set is not part of the type's encoding, so accepting it would silently discard data the caller believes was decoded. Every declared key remains required; this method only constrains what may appear in addition to them.
The one key an object may carry beyond its declared fields is $schema, which
SEP-0051 says any object SHOULD allow for tooling annotation. Callers strip it
before reaching this method, so it never appears in $value.
Keys are echoed through self::safePreview because they originate in caller-supplied JSON; the reported list is capped so a document carrying many unknown keys cannot inflate the message.
Parameters
- $value : array<string|int, mixed>
-
The decoded JSON object,
$schemaalready removed. - $declaredKeys : array<int, string>
-
The type's declared field keys, in IDL order.
- $typeName : string
-
The PHP class the object is being decoded into.
Tags
safePreview()
Return a safe, bounded preview of user-supplied input for use in exception messages.
public
static safePreview(string $s[, int $max = 80 ]) : string
Embedding unbounded user input in exception messages can cause log-amplification denial-of-service when the message is logged. This helper caps the preview at $max bytes and appends "..." when the string is truncated, keeping messages informative without being exploitable.
Parameters
- $s : string
-
The user-supplied string.
- $max : int = 80
-
Maximum bytes before truncation (must be > 3).
Return values
string —A safe, bounded preview.
stringToInt128Parts()
Decompose a signed 128-bit integer string into hi/lo limbs.
public
static stringToInt128Parts(string $value) : array{hi: string, lo: string}
Per XDR Int128Parts (verified against XdrInt128Parts.php decode path): hi is signed int64; lo is unsigned uint64.
Returns an associative array with:
- 'hi': signed int64 base-10 string (range [-2^63, 2^63-1])
- 'lo': unsigned uint64 base-10 string (range [0, 2^64-1])
Parameters
- $value : string
-
Base-10 decimal string of a signed 128-bit integer.
Tags
Return values
array{hi: string, lo: string}stringToInt256Parts()
Decompose a signed 256-bit integer string into four limbs.
public
static stringToInt256Parts(string $value) : array{hiHi: string, hiLo: string, loHi: string, loLo: string}
Per XDR Int256Parts (verified against XdrInt256Parts.php decode path): only hiHi is signed int64; hiLo, loHi, loLo are unsigned uint64. Returns an associative array with:
- 'hiHi': signed int64 base-10 string (range [-2^63, 2^63-1])
- 'hiLo': unsigned uint64 base-10 string (range [0, 2^64-1])
- 'loHi': unsigned uint64 base-10 string (range [0, 2^64-1])
- 'loLo': unsigned uint64 base-10 string (range [0, 2^64-1])
Parameters
- $value : string
-
Base-10 decimal string of a signed 256-bit integer.
Tags
Return values
array{hiHi: string, hiLo: string, loHi: string, loLo: string}stringToInt64()
Parse a base-10 integer string (or a PHP int) to a 64-bit signed int.
public
static stringToInt64(int|string $value) : int
Accepts int|string for compatibility with JSON producers that emit numbers for 64-bit integers in addition to the spec-required strings.
Strict validation rules:
- Empty string is rejected.
- Leading/trailing whitespace is rejected.
- Scientific notation ("1e10") is rejected.
- Decimal points ("1.0") are rejected.
- Hex notation ("0x10") is rejected.
- Leading "+" is rejected.
- Only an optional leading "-" followed by one or more decimal digits is accepted.
- Leading zeros are accepted (SEP-0051 does not forbid them in 64-bit string-encoded integers).
- The resulting value must be in [-2^63, 2^63-1].
- intval() is explicitly NOT used because intval("abc") silently returns 0.
Parameters
- $value : int|string
-
The value to parse.
Tags
Return values
int —The parsed 64-bit signed integer.
stringToUint128Parts()
Decompose an unsigned 128-bit integer string into hi/lo uint64 parts.
public
static stringToUint128Parts(string $value) : array{hi: string, lo: string}
Returns an associative array with keys 'hi' and 'lo', both as base-10 strings representing unsigned uint64 values in [0, 2^64-1].
Parameters
- $value : string
-
Base-10 decimal string of an unsigned 128-bit integer.
Tags
Return values
array{hi: string, lo: string}stringToUint256Parts()
Decompose an unsigned 256-bit integer string into four uint64 limbs.
public
static stringToUint256Parts(string $value) : array{hiHi: string, hiLo: string, loHi: string, loLo: string}
Returns an associative array with keys 'hiHi', 'hiLo', 'loHi', 'loLo', all as base-10 strings representing unsigned uint64 values in [0, 2^64-1].
Parameters
- $value : string
-
Base-10 decimal string of an unsigned 256-bit integer.
Tags
Return values
array{hiHi: string, hiLo: string, loHi: string, loLo: string}stringToUint64()
Parse a base-10 unsigned-integer string (or a PHP int) to a uint64 value.
public
static stringToUint64(int|string $value) : int
Accepts int|string for compatibility with JSON producers that emit numbers for 64-bit integers in addition to the spec-required strings.
For uint64 values above PHP_INT_MAX (i.e. 9223372036854775808..18446744073709551615) a string must be provided; those values cannot be expressed as a PHP native int. The method uses GMP for range validation against [0, 2^64-1] and returns the value as a PHP int, reinterpreting the upper-half as a negative signed int (two's complement) — this matches how the XDR parts structs store uint64 limbs.
Strict validation: same rules as stringToInt64 except no leading minus is allowed.
Parameters
- $value : int|string
-
The value to parse.
Tags
Return values
int —The uint64 value stored as a PHP int (may be negative for values > PHP_INT_MAX).
uint128PartsToString()
Assemble an unsigned 128-bit integer from hi/lo uint64 parts and return as a base-10 string.
public
static uint128PartsToString(string $hi, string $lo) : string
For unsigned 128-bit (UInt128Parts) both the hi and lo limbs are unsigned uint64 values stored as PHP signed ints (two's-complement). GMP is used to reconstruct the full value.
Parameters
- $hi : string
-
Unsigned uint64 as base-10 string. May be a negative base-10 string when the uint64 value exceeds PHP_INT_MAX and is stored as a signed PHP int (the negative form is reinterpreted as the unsigned bit pattern via two's-complement).
- $lo : string
-
Unsigned uint64 as base-10 string. May be a negative base-10 string when the uint64 value exceeds PHP_INT_MAX and is stored as a signed PHP int (the negative form is reinterpreted as the unsigned bit pattern via two's-complement).
Tags
Return values
string —Base-10 decimal string of the assembled 128-bit unsigned integer.
uint256PartsToString()
Assemble an unsigned 256-bit integer from four uint64 limbs and return as a base-10 string.
public
static uint256PartsToString(string $hiHi, string $hiLo, string $loHi, string $loLo) : string
Per XDR UInt256Parts all four limbs are unsigned uint64 values.
Parameters
- $hiHi : string
-
Unsigned uint64 as base-10 string (most significant limb). May be a negative base-10 string when the uint64 value exceeds PHP_INT_MAX and is stored as a signed PHP int (the negative form is reinterpreted as the unsigned bit pattern via two's-complement).
- $hiLo : string
-
Unsigned uint64 as base-10 string (same note as hiHi).
- $loHi : string
-
Unsigned uint64 as base-10 string (same note as hiHi).
- $loLo : string
-
Unsigned uint64 as base-10 string (least significant limb; same note as hiHi).
Tags
Return values
string —Base-10 decimal string of the assembled 256-bit unsigned integer.
uint64ToString()
64-bit unsigned integer to base-10 string (SEP-51 Unsigned Hyper Integer encoding).
public
static uint64ToString(int $value) : string
PHP's int is a 64-bit signed integer on 64-bit systems, so the valid range for the $value parameter is [0, PHP_INT_MAX] (i.e. [0, 2^63-1]). Values in the upper uint64 range (2^63..2^64-1) cannot be represented as a PHP int without overflow; callers holding such values must use stringToUint64() with a string argument instead of this method.
Parameters
- $value : int
-
A PHP int in [0, PHP_INT_MAX].
Tags
Return values
string —Base-10 decimal string.
unescapeString()
Inverse of escapeString: decode a SEP-51 escaped-ASCII string back to raw bytes.
public
static unescapeString(string $escaped) : string
Recognised escape sequences:
- \0 -> 0x00
- \t -> 0x09
- \n -> 0x0A
- \r -> 0x0D
- \ -> 0x5C
- \xNN (exactly two lowercase hex digits) -> byte 0xNN
Any other character following a backslash is rejected. Unescaped printable ASCII characters (0x20..0x7E) are returned as their byte value. The input is validated strictly so that malformed sequences do not silently produce wrong output.
Parameters
- $escaped : string
-
The escaped-ASCII string (output of escapeString).
Tags
Return values
string —Raw binary bytes.
wrapUnsignedToSignedInt()
Wrap an unsigned uint64 base-10 string into the matching PHP signed-int representation used by the Parts struct fields.
public
static wrapUnsignedToSignedInt(string $decimal) : int
The 128-bit / 256-bit Parts wrappers store every limb as a PHP signed int (XDR's hyper / unsigned hyper map onto the same 64-bit native slot on 64-bit systems). Values in [2^63, 2^64-1] therefore land in PHP as negative integers via two's-complement subtraction. This helper performs that conversion once, with full input validation, so the generated parts-decoder bodies do not each carry their own inline closure.
Accepted input: a non-empty string of decimal digits representing an unsigned integer in [0, 2^64-1]. Out-of-range values, leading signs, empty strings, and non-digit characters are rejected.
Parameters
- $decimal : string
-
Unsigned base-10 string in [0, 2^64-1].
Tags
Return values
int —PHP signed int (may be negative for values above PHP_INT_MAX).