From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

/***************************************************************************
copyright : (C) 2021 - 2021 by Dongxu Ma
email : dongxu@cpan.org
This library is free software; you can redistribute it and/or modify
it under MIT license. Refer to LICENSE within the package root folder
for full copyright.
***************************************************************************/
#define PERL_NO_GET_CONTEXT
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#include "ppport.h"
#include <string.h>
#include "jq.h"
#include "jv.h"
// TODO: get version from Alien::LibJQ cflags
#define JQ_VERSION "1.6"
// utility functions for type marshaling
// they are not XS code
jv my_jv_input(pTHX_ void * arg) {
if (arg == NULL) {
return jv_null();
}
SV * const p_sv = arg;
SvGETMAGIC(p_sv);
if (SvTYPE(p_sv) == SVt_NULL || (SvTYPE(p_sv) < SVt_PVAV && !SvOK(p_sv))) {
// undef or JSON::null()
return jv_null();
}
else if (SvROK(p_sv) && SvTYPE(SvRV(p_sv)) == SVt_IV) {
// boolean: \0 or \1, equilvalent of $JSON::PP::true, $JSON::PP::false
//fprintf(stderr, "got boolean value: %s\n", SvTRUE(SvRV(p_sv)) ? "True" : "False");
return jv_bool((bool)SvTRUE(SvRV(p_sv)));
}
else if (SvROK(p_sv) && sv_derived_from(p_sv, "JSON::PP::Boolean")) {
// boolean: $JSON::PP::true and $JSON::PP::false
return jv_bool((bool)SvTRUE(SvRV(p_sv)));
}
else if (SvIOK(p_sv)) {
// integer, use perl's 'native size', see https://perldoc.perl.org/perlguts#What-is-an-%22IV%22?
return jv_number(SvIV(p_sv));
}
else if (SvUOK(p_sv)) {
// unsigned int
return jv_number(SvUV(p_sv));
}
else if (SvNOK(p_sv)) {
// double
return jv_number(SvNV(p_sv));
}
else if (SvPOK(p_sv)) {
// string
STRLEN len;
char * p_pv = SvUTF8(p_sv) ? SvPVutf8(p_sv, len) : SvPV(p_sv, len);
//fprintf(stderr, "my_jv_input() got string: %s\n", p_pv);
return jv_string_sized(p_pv, len);
}
else if (SvROK(p_sv) && SvTYPE(SvRV(p_sv)) == SVt_PVAV) {
// array
jv jval = jv_array();
AV * p_av = (AV *)SvRV(p_sv);
SSize_t len = av_len(p_av);
if (len < 0) {
return jval;
}
SSize_t i;
for (i = 0; i <= len; i++) {
jval = jv_array_append(jval, my_jv_input(aTHX_ *av_fetch(p_av, i, 0)));
}
return jval;
}
else if (SvROK(p_sv) && SvTYPE(SvRV(p_sv)) == SVt_PVHV) {
// hash
jv jval = jv_object();
HV * p_hv = (HV *)SvRV(p_sv);
I32 len = hv_iterinit(p_hv);
I32 i;
for (i = 0; i < len; i++) {
char * key = NULL;
I32 klen = 0;
SV * val = hv_iternextsv(p_hv, &key, &klen);
jval = jv_object_set(jval, jv_string_sized(key, klen), my_jv_input(aTHX_ val));
}
return jval;
}
else {
// not supported
croak("cannot convert perl object to json format: SvTYPE == %i", SvTYPE(p_sv));
}
// NOREACH
}
void * my_jv_output(pTHX_ jv jval) {
jv_kind kind = jv_get_kind(jval);
if (kind == JV_KIND_NULL) {
// null
return newSV(0);
}
else if (kind == JV_KIND_FALSE) {
// boolean: false
// NOTE: get_sv("JSON::PP::false") doesn't work
SV * sv_false = newSV(0);
//fprintf(stderr, "set boolean: False\n");
return sv_setref_iv(sv_false, "JSON::PP::Boolean", 0);
}
else if (kind == JV_KIND_TRUE) {
// boolean: true
SV * sv_true = newSV(0);
//fprintf(stderr, "set boolean: True\n");
return sv_setref_iv(sv_true, "JSON::PP::Boolean", 1);
}
else if (kind == JV_KIND_NUMBER) {
// number
double val = jv_number_value(jval);
SV * p_sv = newSV(0);
if (jv_is_integer(jval)) {
sv_setiv(p_sv, val);
}
else {
sv_setnv(p_sv, val);
}
return p_sv;
}
else if (kind == JV_KIND_STRING) {
// string
//fprintf(stderr, "my_jv_output() got string: %s\n", jv_string_value(jval));
//return newSVpvn(jv_string_value(jval), jv_string_length_bytes(jval));
// NOTE: this might introduce unicode bug..
return newSVpvf("%s", jv_string_value(jval));
}
else if (kind == JV_KIND_ARRAY) {
// array
AV * p_av = newAV();
SSize_t len = (SSize_t)jv_array_length(jv_copy(jval));
av_extend(p_av, len - 1);
SSize_t i;
for (i = 0; i < len; i++) {
jv val = jv_array_get(jv_copy(jval), i);
av_push(p_av, (SV *)my_jv_output(aTHX_ val));
jv_free(val);
}
return newRV_noinc((SV *)p_av);
}
else if (kind == JV_KIND_OBJECT) {
// hash
HV * p_hv = newHV();
int iter = jv_object_iter(jval);
while (jv_object_iter_valid(jval, iter)) {
jv key = jv_object_iter_key(jval, iter);
jv val = jv_object_iter_value(jval, iter);
if (jv_get_kind(key) != JV_KIND_STRING) {
croak("cannot take non-string type as hash key: JV_KIND == %i", jv_get_kind(key));
}
const char * k = jv_string_value(key);
int klen = jv_string_length_bytes(key);
SV * v = (SV *)my_jv_output(aTHX_ val);
hv_store(p_hv, k, klen, v, 0);
jv_free(key);
jv_free(val);
iter = jv_object_iter_next(jval, iter);
}
return newRV_noinc((SV *)p_hv);
}
else {
croak("un-supported jv object type: JV_KIND == %i", kind);
}
// NOREACH
}
static void my_error_cb(void * errors, jv jerr) {
dTHX;
// original jerr will be freed by jq engine
jerr = jv_copy(jerr);
av_push((AV *)errors, newSVpvn(jv_string_value(jerr), jv_string_length_bytes(jerr)));
}
static void my_debug_cb(void * data, jv input) {
dTHX;
int dumpopts = *(int *)data;
jv_dumpf(JV_ARRAY(jv_string("DEBUG:"), input), stderr, dumpopts);
fprintf(stderr, "\n");
}
static inline void assert_isa(pTHX_ SV * self) {
if (!sv_isa(self, "JSON::JQ")) {
croak("self is not a JSON::JQ object");
}
}
// copied from main.c
static const char *skip_shebang(const char *p) {
if (strncmp(p, "#!", sizeof("#!") - 1) != 0)
return p;
const char *n = strchr(p, '\n');
if (n == NULL || n[1] != '#')
return p;
n = strchr(n + 1, '\n');
if (n == NULL || n[1] == '#' || n[1] == '\0' || n[-1] != '\\' || n[-2] == '\\')
return p;
n = strchr(n + 1, '\n');
if (n == NULL)
return p;
return n+1;
}
MODULE = JSON::JQ PACKAGE = JSON::JQ
PROTOTYPES: DISABLE
int
JV_PRINT_INDENT_FLAGS(n)
int n
CODE:
RETVAL = JV_PRINT_INDENT_FLAGS(n);
OUTPUT:
RETVAL
void
_init(self)
HV * self
INIT:
jq_state * _jq = NULL;
SV * sv_jq;
HV * hv_attr;
char * script;
AV * av_err;
int compiled = 0;
CODE:
assert_isa(aTHX_ ST(0));
// step 1. initialize
_jq = jq_init();
if (_jq == NULL) {
croak("cannot malloc jq engine");
}
else {
sv_jq = newSV(0);
sv_setiv(sv_jq, PTR2IV(_jq));
SvREADONLY_on(sv_jq);
hv_stores(self, "_jq", sv_jq);
}
// step 2. set error and debug callbacks
av_err = (AV *)SvRV(*hv_fetchs(self, "_errors", 0));
jq_set_error_cb(_jq, my_error_cb, av_err);
int dumpopts = (int)SvIV(*hv_fetchs(self, "_dumpopts", 0));
jq_set_debug_cb(_jq, my_debug_cb, &dumpopts);
// step 3. set initial attributes
hv_attr = (HV *)SvRV(*hv_fetchs(self, "_attribute", 0));
I32 len = hv_iterinit(hv_attr);
I32 i;
for (i = 0; i < len; i++) {
char * key = NULL;
I32 klen = 0;
SV * val = hv_iternextsv(hv_attr, &key, &klen);
jq_set_attr(_jq, jv_string_sized(key, klen), my_jv_input(aTHX_ val));
}
// set JQ_VERSION
jq_set_attr(_jq, jv_string("VERSION_DIR"), jv_string(JQ_VERSION));
// step 4. compile
jv args = my_jv_input(aTHX_ *hv_fetchs(self, "variable", 0));
if (hv_exists(self, "script_file", 11)) {
jv data = jv_load_file(SvPV_nolen(*hv_fetchs(self, "script_file", 0)), 1);
if (!jv_is_valid(data)) {
data = jv_invalid_get_msg(data);
my_error_cb(av_err, data);
jv_free(data);
XSRETURN_NO;
}
compiled = jq_compile_args(_jq, skip_shebang(jv_string_value(data)), args);
jv_free(data);
}
else {
script = SvPV_nolen(*hv_fetchs(self, "script", 0));
compiled = jq_compile_args(_jq, script, args);
}
if (compiled) {
if (SvTRUE(get_sv("JSON::JQ::DUMP_DISASM", 0))) {
jq_dump_disassembly(_jq, 0);
printf("\n");
}
XSRETURN_YES;
}
else {
// args freed by jq engine
//jv_free(args);
// jq_teardown(&_jq); // no need to call destructor here, DESTROY will do
XSRETURN_NO;
}
int
_process(self, sv_input, av_output)
HV * self
SV * sv_input
AV * av_output
INIT:
jq_state * _jq = NULL;
SV * sv_jq;
AV * av_err;
CODE:
assert_isa(aTHX_ ST(0));
sv_jq = *hv_fetchs(self, "_jq", 0);
_jq = INT2PTR(jq_state *, SvIV(sv_jq));
jv jv_input = my_jv_input(aTHX_ sv_input);
int jq_flags = (int)SvIV(*hv_fetchs(self, "jq_flags", 0));
// logic from process() in main.c
jq_start(_jq, jv_input, jq_flags);
jv result;
// clear previous call errors
av_err = (AV *)SvRV(*hv_fetchs(self, "_errors", 0));
av_clear(av_err);
int ret = 14;
while (jv_is_valid(result = jq_next(_jq))) {
av_push(av_output, (SV *)my_jv_output(aTHX_ result));
if (jv_get_kind(result) == JV_KIND_FALSE || jv_get_kind(result) == JV_KIND_NULL) {
ret = 11;
}
else {
ret = 0;
}
//jv_free(result);
}
if (jq_halted(_jq)) {
// jq program invoked `halt` or `halt_error`
jv exit_code = jq_get_exit_code(_jq);
if (!jv_get_kind(exit_code)) {
ret = 0;
}
else if (jv_get_kind(exit_code) == JV_KIND_NUMBER) {
ret = jv_number_value(exit_code);
}
else {
ret = 5;
}
jv_free(exit_code);
jv error_message = jq_get_error_message(_jq);
if (jv_get_kind(error_message) == JV_KIND_STRING) {
my_error_cb(av_err, error_message);
}
else if (jv_get_kind(error_message) == JV_KIND_NULL) {
// halt with no output
}
else if (jv_is_valid(error_message)) {
error_message = jv_dump_string(jv_copy(error_message), 0);
my_error_cb(av_err, error_message);
}
else {
// no message; use --debug-trace to see a message
}
jv_free(error_message);
}
else if (jv_invalid_has_msg(jv_copy(result))) {
// uncaught jq exception
jv msg = jv_invalid_get_msg(jv_copy(result));
//jv input_pos = jq_util_input_get_position(_jq);
if (jv_get_kind(msg) == JV_KIND_STRING) {
//av_push(av_err, newSVpvf("jq: error (at %s): %s", jv_string_value(input_pos), jv_string_value(msg)));
av_push(av_err, newSVpvf("jq: error: %s", jv_string_value(msg)));
}
else {
msg = jv_dump_string(msg, 0);
//av_push(av_err, newSVpvf("jq: error (at %s) (not a string): %s", jv_string_value(input_pos), jv_string_value(msg)));
av_push(av_err, newSVpvf("jq: error (not a string): %s", jv_string_value(msg)));
}
ret = 5;
//jv_free(input_pos);
jv_free(msg);
}
jv_free(result);
RETVAL = ret;
OUTPUT:
RETVAL
void
DESTROY(self)
HV * self
INIT:
jq_state * _jq = NULL;
SV * sv_jq;
CODE:
assert_isa(aTHX_ ST(0));
sv_jq = *hv_fetchs(self, "_jq", 0);
_jq = INT2PTR(jq_state *, SvIV(sv_jq));
if (_jq != NULL) {
if (SvTRUE(get_sv("JSON::JQ::DEBUG", 0))) {
fprintf(stderr, "destroying jq object: %p\n", _jq);
}
jq_teardown(&_jq);
}