use
constant
JSON_XS
=>
$ENV
{MOJO_NO_JSON_XS}
? 0
use
constant
CORE_BOOLS
=>
defined
&builtin::is_bool
;
BEGIN {
warnings->unimport(
'experimental::builtin'
)
if
CORE_BOOLS;
}
our
@EXPORT_OK
=
qw(decode_json encode_json false from_json j to_json true)
;
my
%ESCAPE
= (
'"'
=>
'"'
,
'\\'
=>
'\\'
,
'/'
=>
'/'
,
'b'
=>
"\x08"
,
'f'
=>
"\x0c"
,
'n'
=>
"\x0a"
,
'r'
=>
"\x0d"
,
't'
=>
"\x09"
);
my
%REVERSE
=
map
{
$ESCAPE
{
$_
} =>
"\\$_"
}
keys
%ESCAPE
;
for
(0x00 .. 0x1f) {
$REVERSE
{
pack
'C'
,
$_
} //=
sprintf
'\u%.4X'
,
$_
}
if
(JSON_XS) {
my
$BINARY
= Cpanel::JSON::XS->new->utf8;
my
$TEXT
= Cpanel::JSON::XS->new;
$_
->canonical->allow_nonref->allow_unknown->allow_blessed->convert_blessed->stringify_infnan->escape_slash
->allow_dupkeys
for
$BINARY
,
$TEXT
;
monkey_patch __PACKAGE__,
'encode_json'
,
sub
{
$BINARY
->encode(
$_
[0]) };
monkey_patch __PACKAGE__,
'decode_json'
,
sub
{
$BINARY
->decode(
$_
[0]) };
monkey_patch __PACKAGE__,
'to_json'
,
sub
{
$TEXT
->encode(
$_
[0]) };
monkey_patch __PACKAGE__,
'from_json'
,
sub
{
$TEXT
->decode(
$_
[0]) };
}
sub
decode_json {
my
$err
= _decode(\
my
$value
,
shift
);
return
defined
$err
? croak
$err
:
$value
;
}
sub
encode_json { encode(
'UTF-8'
, _encode_value(
shift
)) }
sub
false () {JSON::PP::false}
sub
from_json {
my
$err
= _decode(\
my
$value
,
shift
, 1);
return
defined
$err
? croak
$err
:
$value
;
}
sub
j {
return
encode_json(
$_
[0])
if
ref
$_
[0] eq
'ARRAY'
||
ref
$_
[0] eq
'HASH'
;
return
scalar
eval
{ decode_json(
$_
[0]) };
}
sub
to_json { _encode_value(
shift
) }
sub
true () {JSON::PP::true}
sub
_decode {
my
$valueref
=
shift
;
eval
{
die
"Missing or empty input at offset 0\n"
unless
length
(
local
$_
=
shift
);
$_
= decode(
'UTF-8'
,
$_
)
unless
shift
;
die
"Input is not UTF-8 encoded\n"
unless
defined
;
$$valueref
= _decode_value();
/\G[\x20\x09\x0a\x0d]*\z/gc or _throw(
'Unexpected data'
);
} ?
return
undef
:
chomp
$@;
return
$@;
}
sub
_decode_array {
my
@array
;
until
(m/\G[\x20\x09\x0a\x0d]*\]/gc) {
push
@array
, _decode_value();
redo
if
/\G[\x20\x09\x0a\x0d]*,/gc;
last
if
/\G[\x20\x09\x0a\x0d]*\]/gc;
_throw(
'Expected comma or right square bracket while parsing array'
);
}
return
\
@array
;
}
sub
_decode_object {
my
%hash
;
until
(m/\G[\x20\x09\x0a\x0d]*\}/gc) {
/\G[\x20\x09\x0a\x0d]*"/gc or _throw(
'Expected string while parsing object'
);
my
$key
= _decode_string();
/\G[\x20\x09\x0a\x0d]*:/gc or _throw(
'Expected colon while parsing object'
);
$hash
{
$key
} = _decode_value();
redo
if
/\G[\x20\x09\x0a\x0d]*,/gc;
last
if
/\G[\x20\x09\x0a\x0d]*\}/gc;
_throw(
'Expected comma or right curly bracket while parsing object'
);
}
return
\
%hash
;
}
sub
_decode_string {
my
$pos
=
pos
;
m!\G((?:(?:[^\x00-\x1f\\
"]|\\(?:["
\\/bfnrt]|u[0-9a-fA-F]{4})){0,32766})*)!gc;
my
$str
= $1;
unless
(m/\G"/gc) {
_throw(
'Unexpected character or invalid escape while parsing string'
)
if
/\G[\x00-\x1f\\]/;
_throw(
'Unterminated string'
);
}
if
(
index
(
$str
,
'\\u'
) < 0) {
$str
=~ s!\\(["\\/bfnrt])!
$ESCAPE
{$1}!gs;
return
$str
;
}
my
$buffer
=
''
;
while
(
$str
=~ /\G([^\\]*)\\(?:([^u])|u(.{4}))/gc) {
$buffer
.= $1;
if
($2) {
$buffer
.=
$ESCAPE
{$2} }
else
{
my
$ord
=
hex
$3;
if
((
$ord
& 0xf800) == 0xd800) {
(
$ord
& 0xfc00) == 0xd800 or
pos
=
$pos
+
pos
(
$str
), _throw(
'Missing high-surrogate'
);
$str
=~ /\G\\u([Dd][C-Fc-f]..)/gc or
pos
=
$pos
+
pos
(
$str
), _throw(
'Missing low-surrogate'
);
$ord
= 0x10000 + (
$ord
- 0xd800) * 0x400 + (
hex
($1) - 0xdc00);
}
$buffer
.=
pack
'U'
,
$ord
;
}
}
return
$buffer
.
substr
$str
,
pos
(
$str
),
length
(
$str
);
}
sub
_decode_value {
/\G[\x20\x09\x0a\x0d]*/gc;
return
_decode_string()
if
/\G"/gc;
return
_decode_object()
if
/\G\{/gc;
return
_decode_array()
if
/\G\[/gc;
return
0 + $1
if
/\G([-]?(?:0|[1-9][0-9]*)(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?)/gc;
return
true()
if
/\Gtrue/gc;
return
false()
if
/\Gfalse/gc;
return
undef
if
/\Gnull/gc;
_throw(
'Expected string, array, object, number, boolean or null'
);
}
sub
_encode_array {
'['
.
join
(
','
,
map
{ _encode_value(
$_
) } @{
$_
[0]}) .
']'
;
}
sub
_encode_object {
my
$object
=
shift
;
my
@pairs
=
map
{ _encode_string(
$_
) .
':'
. _encode_value(
$object
->{
$_
}) }
sort
keys
%$object
;
return
'{'
.
join
(
','
,
@pairs
) .
'}'
;
}
sub
_encode_string {
my
$str
=
shift
;
$str
=~ s!([\x00-\x1f\\"/])!
$REVERSE
{$1}!gs;
return
"\"$str\""
;
}
sub
_encode_value {
my
$value
=
shift
;
if
(
my
$ref
=
ref
$value
) {
return
_encode_object(
$value
)
if
$ref
eq
'HASH'
;
return
_encode_array(
$value
)
if
$ref
eq
'ARRAY'
;
return
$$value
?
'true'
:
'false'
if
$ref
eq
'SCALAR'
;
return
$value
?
'true'
:
'false'
if
$ref
eq
'JSON::PP::Boolean'
;
return
'null'
unless
blessed
$value
;
return
_encode_string(
$value
)
unless
my
$sub
=
$value
->can(
'TO_JSON'
);
return
_encode_value(
$value
->
$sub
);
}
return
'null'
unless
defined
$value
;
return
$value
?
'true'
:
'false'
if
CORE_BOOLS && builtin::is_bool(
$value
);
no
warnings
'numeric'
;
return
$value
if
!utf8::is_utf8(
$value
) &&
length
((
my
$dummy
=
''
) &
$value
) && 0 +
$value
eq
$value
&&
$value
* 0 == 0;
return
_encode_string(
$value
);
}
sub
_throw {
/\G[\x20\x09\x0a\x0d]*/gc;
my
$context
=
'Malformed JSON: '
.
shift
;
if
(m/\G\z/gc) {
$context
.=
' before end of data'
}
else
{
my
@lines
=
split
/\n/,
substr
(
$_
, 0,
pos
);
$context
.=
' at line '
.
@lines
.
', offset '
.
length
(
pop
@lines
||
''
);
}
die
"$context\n"
;
}
1;