OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-atomspace-storage / opencog / persist / json / DecodeJson.cc

645 lines · cc ref:
/*
 * DecodeJson.cc
 * Decode JSON describing Atoms and Values.
 *
 * Copyright (c) 2019, 2022 Linas Vepstas <linas@linas.org>
 * SPDX-License-Identifier: AGPL-3.0-or-later
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License v3 as
 * published by the Free Software Foundation and including the exceptions
 * at http://opencog.org/wiki/Licenses
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, write to:
 * Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */
#include <iomanip>

#include <opencog/atoms/base/Atom.h>
#include <opencog/atoms/base/Node.h>
#include <opencog/atoms/base/Link.h>
#include <opencog/atoms/value/FloatValue.h>
#include <opencog/atoms/value/LinkValue.h>
#include <opencog/atoms/value/StringValue.h>
#include <opencog/atoms/value/ValueFactory.h>
#include <opencog/atomspace/AtomSpace.h>

#include "Json.h"
#include "../sexpr/Sexpr.h"

using namespace opencog;

// This does NOT use any external JSON decoder libraries because those
// libs don't really make anything simpler, and also we don't need most
// of the features that they offer, and also I don't want more
// dependencies in the AtomSpace.

/* ================================================================== */
/**
 * Look for a type name of the form "ConceptNode" (with quotes)
 * starting at location `pos` in `tna`.
 * Return the type and update `pos` to point after the typename.
 */
Type Json::decode_type(const std::string& tna, size_t& pos)
{
	// Advance past whitespace.
	pos = tna.find_first_not_of(" \n\t", pos);
	if (std::string::npos == pos)
		throw SyntaxException(TRACE_INFO, "Bad Type >>%s<<",
			tna.substr(pos).c_str());

	// Check if we have a quoted string
	if ('"' == tna[pos]) {
		pos++; // skip opening quote
		size_t endquote = tna.find('"', pos);
		if (std::string::npos == endquote)
			throw SyntaxException(TRACE_INFO, "Unterminated quoted type >>%s<<",
				tna.substr(pos).c_str());

		Type t = nameserver().getType(tna.substr(pos, endquote-pos));
		if (NOTYPE == t)
			throw SyntaxException(TRACE_INFO, "Unknown Type >>%s<<",
				tna.substr(pos, endquote-pos).c_str());

		pos = endquote + 1; // skip closing quote
		return t;
	}

	// Unquoted string - advance to next whitespace
	size_t nos = tna.find_first_of(",) \n\t", pos);
	if (std::string::npos == nos)
		nos = tna.size();

	Type t = nameserver().getType(tna.substr(pos, nos-pos));
	if (NOTYPE == t)
		throw SyntaxException(TRACE_INFO, "Unknown Type >>%s<<",
			tna.substr(pos, nos-pos).c_str());

	pos = nos;
	return t;
}

/* ================================================================== */

/**
 * Decode a type argument that can be either a simple string like
 * "ConceptNode" or a JSON object like {"type": "ConceptNode"}.
 * It might also be a field in a JSON object:
 *   {"other": "stuff", "type": "ConceptNode", "more": "stuff"}
 */
Type Json::decode_type_arg(const std::string& tna, size_t& pos)
{
	// Advance past whitespace.
	pos = tna.find_first_not_of(" \n\t", pos);
	if (std::string::npos == pos)
		throw SyntaxException(TRACE_INFO, "Bad Type - empty string");

	// Check if this is a JSON object format
	if ('{' == tna[pos])
	{
		// Save the start position of the JSON object
		size_t obj_start = pos;

		// Look for "type": field
		size_t tpos = tna.find("\"type\":", pos);
		if (std::string::npos == tpos)
			throw SyntaxException(TRACE_INFO, "Missing type field in JSON object");

		tpos += 7; // skip past "type":

		// Call the original decode_type to parse the actual type name
		Type t = decode_type(tna, tpos);

		// For MCP format, we're looking at a params object that might have
		// other fields before "type". We need to find the closing brace
		// of the entire params object, not just the first closing brace.
		// We'll count braces but also handle string literals properly.
		size_t scan_pos = obj_start;
		int brace_count = 0;
		size_t close = std::string::npos;
		bool in_string = false;
		bool escape_next = false;

		while (scan_pos < tna.size()) {
			char ch = tna[scan_pos];

			if (escape_next) {
				escape_next = false;
			} else if (ch == '\\' && in_string) {
				escape_next = true;
			} else if (ch == '"') {
				in_string = !in_string;
			} else if (!in_string) {
				if (ch == '{') {
					brace_count++;
				} else if (ch == '}') {
					brace_count--;
					if (brace_count == 0) {
						close = scan_pos;
						break;
					}
				}
			}
			scan_pos++;
		}

		if (std::string::npos == close)
			throw SyntaxException(TRACE_INFO, "Missing closing brace >>%s<<",
				tna.substr(obj_start).c_str());

		// Update pos to point after the closing brace.
		pos = close + 1;

		return t;
	}
	else
	{
		// Simple string format - just call decode_type directly
		return decode_type(tna, pos);
	}
}

/* ================================================================== */

/// Extracts Node name-string. Given the string `s`, this updates
/// the `l` and `r` values such that `l` points at the first
/// non-whitespace character of the name, and `r` points at the last.
/// The string is considered to start *after* the first quote, and ends
/// just before the last quote. In this case, escaped quotes \" are
/// ignored (are considered to be part of the string).
///
/// This returns the unescaped node name.
///
std::string Json::get_node_name(const std::string& s,
                                size_t& l, size_t& r)
{
	// Advance past whitespace.
	while (l < r and (s[l] == ' ' or s[l] == '\t' or s[l] == '\n')) l++;

	l++;
	size_t p = l;
	for (; p < r and (s[p] != '"' or ((0 < p) and (s[p - 1] == '\\'))); p++);
	r = p;

	// We use std::quoted() to unescape embedded quotes.
	// Unescaping works ONLY if the leading character is a quote!
	// So readjust left and right to pick those up.
	if ('"' == s[l-1]) l--; // grab leading quote, for std::quoted().
	if ('"' == s[r]) r++;   // step past trailing quote.
	std::stringstream ss;
	std::string name;
	ss << s.substr(l, r-l);
	ss >> std::quoted(name);
	return name;
}

/* ================================================================== */

/**
 * Get a node name argument that can be either a direct string
 * like "foo" or from a JSON object like {"name": "foo"}.
 */
std::string Json::get_node_name_arg(const std::string& s, size_t& pos, size_t& r)
{
	// Advance past whitespace.
	pos = s.find_first_not_of(" \n\t", pos);
	if (std::string::npos == pos)
		throw SyntaxException(TRACE_INFO, "Bad node name - empty string");

	// Check if this is a JSON object format
	if ('{' == s[pos])
	{
		// Look for "name": field
		size_t npos = s.find("\"name\":", pos);
		if (std::string::npos == npos)
			throw SyntaxException(TRACE_INFO, "Missing name field in JSON object");

		npos += 7; // skip past "name":

		// Find the closing brace to use as boundary
		size_t close = s.find('}', npos);
		if (std::string::npos == close)
			throw SyntaxException(TRACE_INFO, "Missing closing brace >>%s<<",
				s.substr(npos).c_str());

		// Call get_node_name to extract the actual name
		std::string name = get_node_name(s, npos, close);

		// Update pos to point after the closing brace
		pos = close + 1;

		return name;
	}
	else
	{
		// Simple string format - just call get_node_name directly
		return get_node_name(s, pos, r);
	}
}

/* ================================================================== */

/// Helper function to find the closing delimiter of a JSON object or array
/// For objects (starting with '{'), returns the position after the closing '}'
/// For arrays (starting with '['), returns the position after the closing ']'
/// Returns string::npos if not found or if start doesn't begin with '{' or '['
static size_t find_json_end(const std::string& s, size_t start)
{
	if (start >= s.size()) return std::string::npos;

	char open_char = s[start];

	if (open_char != '{' && open_char != '[') {
		return std::string::npos;
	}

	int depth = 1;
	size_t pos = start + 1;
	bool in_string = false;
	bool escape_next = false;

	while (pos < s.size() && depth > 0) {
		char ch = s[pos];
		if (escape_next) {
			escape_next = false;
		} else if (ch == '\\' && in_string) {
			escape_next = true;
		} else if (ch == '"') {
			in_string = !in_string;
		} else if (!in_string) {
			if (ch == open_char) {
				// Only count the same type of delimiter we're looking for
				depth++;
			} else if ((open_char == '{' && ch == '}') ||
			           (open_char == '[' && ch == ']')) {
				depth--;
				if (depth == 0) {
					// Found the matching closing delimiter
					return pos + 1; // Return position after the delimiter
				}
			}
		}
		pos++;
	}

	return (depth == 0) ? pos : std::string::npos;
}

/// Helper function to find a top-level field in a JSON object
/// Returns the position after the colon, or string::npos if not found
static size_t find_top_level_field(const std::string& s,
                                   const std::string& field_name,
                                   size_t obj_start, size_t obj_end)
{
	std::string search_pattern = "\"" + field_name + "\":";
	size_t search_pos = obj_start + 1;
	int depth = 0;
	bool in_string = false;
	bool escape_next = false;

	while (search_pos < obj_end) {
		char ch = s[search_pos];
		if (escape_next) {
			escape_next = false;
		} else if (ch == '\\' && in_string) {
			escape_next = true;
		} else if (ch == '"') {
			in_string = !in_string;
			// When we exit a string at depth 0, check if it's our field
			if (!in_string && depth == 0) {
				// We just exited a string, check if it matches our pattern
				// Make sure we have enough characters before search_pos
				// We need at least field_name.length() + 2 chars (for quotes) before search_pos
				if (search_pos >= field_name.length() + 2) {
					size_t check_start = search_pos - field_name.length() - 1;
					// Additional bounds check
					if (check_start >= obj_start &&
					    check_start + search_pattern.length() <= s.length() &&
					    s.compare(check_start, search_pattern.length(), search_pattern) == 0) {
						return check_start + search_pattern.length();
					}
				}
			}
		} else if (!in_string) {
			if (ch == '{' || ch == '[') {
				depth++;
			} else if (ch == '}' || ch == ']') {
				depth--;
			}
		}
		search_pos++;
	}

	return std::string::npos;
}

/* ================================================================== */

/// Convert an Atomese JSON expression into a C++ Atom.
/// For example: `{ "type": "Concept", "name": "foo" }`
/// will return the corresponding atom.
/// Reversed arguments `{ "name": "foo", "type": "Concept" }`
/// should also work.
///
/// The string to decode is `s`, beginning at location `l` and using `r`
/// as a hint for the end of the expression.
///
Handle Json::decode_atom(const std::string& s,
                         size_t& l, size_t& r)
{
	// Find the start of the JSON object
	l = s.find_first_not_of(" \n\t", l);
	if (std::string::npos == l or s[l] != '{')
		return Handle::UNDEFINED;

	// Find the end of this JSON object
	size_t obj_start = l;
	size_t obj_end = find_json_end(s, obj_start);
	if (std::string::npos == obj_end) return Handle::UNDEFINED;

	// Check for "atomese" field - if present, decode as s-expression
	size_t apos = find_top_level_field(s, "atomese", obj_start, obj_end);
	if (std::string::npos != apos)
	{
		// Find the start of the string value
		apos = s.find_first_not_of(" \n\t", apos);
		if (std::string::npos == apos or '"' != s[apos])
			return Handle::UNDEFINED;

		// Extract the quoted string and unescape it
		std::stringstream ss;
		ss << s.substr(apos);
		std::string sexpr_str;
		ss >> std::quoted(sexpr_str);

		// Decode using s-expression decoder
		size_t sexpr_pos = 0;
		Handle h = Sexpr::decode_atom(sexpr_str, sexpr_pos);

		// Update r to point after the closing brace of the JSON object
		r = obj_end;
		return h;
	}

	// Find the "type" field at the top level
	size_t tpos = find_top_level_field(s, "type", obj_start, obj_end);
	if (std::string::npos == tpos) return Handle::UNDEFINED;

	Type t = NOTYPE;
	try {
		t = Json::decode_type(s, tpos);
	}
	catch(...) {
		return Handle::UNDEFINED;
	}

	if (nameserver().isA(t, NODE))
	{
		// Find the "name" field at the top level
		size_t apos = find_top_level_field(s, "name", obj_start, obj_end);
		if (std::string::npos == apos) return Handle::UNDEFINED;

		apos = s.find_first_not_of(" \n\t", apos);
		size_t name_end = obj_end;
		std::string name = Json::get_node_name(s, apos, name_end);

		// Update r to point after the closing brace of the JSON object
		r = obj_end;

		return createNode(t, std::move(name));
	}

	if (nameserver().isA(t, LINK))
	{
		// Find the "outgoing" field at the top level
		size_t opos = find_top_level_field(s, "outgoing", obj_start, obj_end);
		if (std::string::npos == opos) return Handle::UNDEFINED;

		// Find the opening bracket of the outgoing array
		l = s.find("[", opos);
		if (std::string::npos == l) return Handle::UNDEFINED;

		// Find the closing bracket using our helper function
		size_t array_end = find_json_end(s, l);
		if (std::string::npos == array_end) return Handle::UNDEFINED;

		l++; // Move past the '['
		size_t close_bracket = array_end - 1; // Position of the ']'

		HandleSeq hs;

		// Process each atom in the outgoing array
		while (l < close_bracket)
		{
			// Skip whitespace and commas
			l = s.find_first_not_of(" \n\t,", l);
			if (l >= close_bracket || std::string::npos == l) break;

			// Check if we've reached the end of the array
			if (s[l] == ']') break;

			size_t atom_end = close_bracket;
			Handle ho = Json::decode_atom(s, l, atom_end);
			if (nullptr == ho) return Handle::UNDEFINED;
			hs.push_back(ho);

			// Update position for next iteration
			l = atom_end;
		}

		// Update r to point after the closing brace of the JSON object
		r = obj_end;

		return createLink(std::move(hs), t);
	}

	return Handle::UNDEFINED;
}

/* ================================================================== */

#define GET_VALUE_POS \
	size_t opos = s.find("\"value\":", l); \
	if (std::string::npos != opos) \
		opos += 8;  /* skip past "value": */ \
	else { \
		opos = s.find("\"values\":", l); \
		if (std::string::npos == opos) return nullptr; \
		opos += 9;  /* skip past "values": */ \
	}

/// Convert an Atomese JSON expression into a C++ Value.
/// For example: `{ "type": "FloatValue", "value": [1, 2, 3] }`
/// will return the corresponding ValuePtr.
///
/// The string to decode is `s`, beginning at location `l` and using `r`
/// as a hint for the end of the expression.
///
ValuePtr Json::decode_value(const std::string& s,
                            size_t& lo, size_t& ro)
{
	size_t l = lo;
	l = s.find("{", l);
	if (std::string::npos == l) return nullptr;

	// Find the end of this JSON object
	size_t obj_start = l;
	size_t obj_end = find_json_end(s, obj_start);
	if (std::string::npos == obj_end) return nullptr;

	// Look for tunneled s-expressions
	size_t apos = find_top_level_field(s, "atomese", obj_start, obj_end);
	if (std::string::npos != apos)
	{
		// Find the start of the string value
		apos = s.find_first_not_of(" \n\t", apos);
		if (std::string::npos == apos or '"' != s[apos])
			return nullptr;

		// Extract the quoted string and unescape it
		std::stringstream ss;
		ss << s.substr(apos);
		std::string sexpr_str;
		ss >> std::quoted(sexpr_str);

		// Decode using s-expression decoder
		size_t sexpr_pos = 0;
		ValuePtr v = Sexpr::decode_value(sexpr_str, sexpr_pos);

		// Update ro to point after the closing brace of the JSON object
		ro = obj_end;
		return v;
	}

	size_t tpos = s.find("\"type\":", l);
	if (std::string::npos == tpos) return nullptr;
	tpos += 7;  // skip past "type":

	Type t = NOTYPE;
	try {
		t = Json::decode_type(s, tpos);
	}
	catch(...) {
		return nullptr;
	}

	if (nameserver().isA(t, ATOM))
	{
		return decode_atom(s, lo, ro);
	}

	if (nameserver().isA(t, FLOAT_VALUE))
	{
		l = tpos;
		GET_VALUE_POS;
		l = s.find("[", opos);

		size_t r = ro;
		std::vector<double> vd;
		while (std::string::npos != l)
		{
			l++;
			r = s.find_first_of(",]", l);
			if (std::string::npos == r) break;

			// Bad clients can (and do) send mal-formed JSON.
			// This includes the empty list [] and expressions
			// with a trailing comma. Skip out of those.
			if (r == l) break;

			std::stringstream ss;
			ss << s.substr(l, r-l);
			double d;
			ss >> d;
			vd.push_back(d);

			if (']' == s[r]) break;
			l = r;
		}

		r = s.find("}", r);
		ro = r;
		return valueserver().create(t, std::move(vd));
	}

	if (nameserver().isA(t, STRING_VALUE))
	{
		l = tpos;
		GET_VALUE_POS;
		l = s.find("[", opos);

		size_t r = ro;
		std::vector<std::string> vs;
		while (std::string::npos != l)
		{
			l++;

			// Bad clients can (and do) send mal-formed JSON.
			// This includes the empty list [] and expressions
			// with a trailing comma. Skip out of those.
			if (']' == s[l]) break;

			std::stringstream ss;
			ss << s.substr(l);
			std::string uq;
			ss >> std::quoted(uq);
			vs.push_back(uq);

			// Step past the closing quote.
			r = l + uq.size() + 2;

			// Get to the next elt
			r = s.find_first_of(",]", r);
			if (std::string::npos == r) break;
			if (']' == s[r]) break;
			l = r;
		}

		r = s.find("}", r);
		ro = r;
		return valueserver().create(t, std::move(vs));
	}

	if (nameserver().isA(t, LINK_VALUE))
	{
		l = tpos;
		GET_VALUE_POS;
		l = s.find("[", opos);

		size_t r = ro;
		std::vector<ValuePtr> vv;
		while (std::string::npos != l)
		{
			l++;
			ValuePtr vp = decode_value(s, l, r);

			// Bad clients can (and do) send mal-formed JSON.
			// This includes the empty list [] and expressions
			// with a trailing comma. Skip out of those.
			if (nullptr == vp) break;
			vv.push_back(vp);

			// Get to the next elt
			r = s.find_first_of(",]", r);
			if (std::string::npos == r) break;
			if (']' == s[r]) break;
			l = r;
		}

		r = s.find("}", r);
		ro = r;
		return valueserver().create(t, std::move(vv));
	}

	return nullptr;
}

/* ============================= END OF FILE ================= */