// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This file contains bindings to convert from JSON to C++ base::Value objects.
// The code related to `base::Value` can be found in 'values.rs'
// and 'values_deserialization.rs'.

use crate::values::ValueSlotRef;
use crate::values_deserialization::ValueVisitor;
use cxx::CxxString;
use serde::de::Deserializer;
use serde_json_lenient::de::SliceRead;
use std::pin::Pin;

// UTF8 byte order mark.
const UTF8_BOM: [u8; 3] = [0xef, 0xbb, 0xbf];

#[cxx::bridge(namespace=base::rs_glue)]
mod ffi {
    /// Options for parsing JSON inputs. Largely a mirror of the C++ `base::JSONParserOptions`
    /// bitflags, represented as a friendlier struct-of-bools instead.
    #[namespace=base::ffi::json::json_parser]
    struct JsonOptions {
        /// Allows commas to exist after the last element in structures.
        allow_trailing_commas: bool,
        /// If set the parser replaces invalid code points (i.e. lone surrogates) with the Unicode
        /// replacement character (U+FFFD). If not set, invalid code points trigger a hard error and
        /// parsing fails.
        replace_invalid_characters: bool,
        /// Allows both C (/* */) and C++ (//) style comments.
        allow_comments: bool,
        /// Permits unescaped ASCII control characters (such as unescaped \r and \n) in the range
        /// [0x00,0x1F].
        allow_control_chars: bool,
        /// Permits \\v vertical tab escapes.
        allow_vert_tab: bool,
        /// Permits \\xNN escapes as described above.
        allow_x_escapes: bool,
        /// The maximum recursion depth to walk while parsing nested JSON objects. JSON beyond the
        /// specified depth will be ignored.
        max_depth: usize,
    }

    unsafe extern "C++" {
        include!("base/rs_glue/values_glue.h");
        type ValueSlot = crate::rs_glue::ffi::ValueSlot;
    }

    extern "Rust" {
        #[namespace=base::ffi::json::json_parser]
        pub fn decode_json_from_cpp(
            json: &[u8],
            options: JsonOptions,
            value_slot: Pin<&mut ValueSlot>,
            error_line: &mut i32,
            error_column: &mut i32,
            error_message: Pin<&mut CxxString>,
        ) -> bool;
    }
}

pub type JsonOptions = ffi::JsonOptions;
impl JsonOptions {
    /// Construct a JsonOptions with common Chromium extensions.
    ///
    /// Per base::JSONParserOptions::JSON_PARSE_CHROMIUM_EXTENSIONS:
    ///
    /// This parser historically accepted, without configuration flags,
    /// non-standard JSON extensions. This enables that traditional parsing
    /// behavior.
    pub fn with_chromium_extensions(max_depth: usize) -> JsonOptions {
        JsonOptions {
            allow_trailing_commas: false,
            replace_invalid_characters: false,
            allow_comments: true,
            allow_control_chars: true,
            allow_vert_tab: true,
            allow_x_escapes: true,
            max_depth,
        }
    }
}

/// Decode some JSON into C++ base::Value object tree.
///
/// This function takes and returns normal Rust types. For an equivalent which
/// can be called from C++, see `decode_json_from_cpp`.
///
/// # Args:
///
/// * `json`: the JSON. Note that this is a slice of bytes rather than a string,
///           which in Rust terms means it hasn't yet been validated to be
///           legitimate UTF8. The JSON decoding will do that.
/// * `options`: configuration options for non-standard JSON extensions
/// * `value_slot`: a space into which to construct a base::Value
///
/// It always strips a UTF8 BOM from the start of the string, if one is found.
///
/// Return: a serde_json_lenient::Error or Ok.
///
/// It is be desirable in future to expose this API to other Rust code inside
/// and outside //base. TODO(crbug/1287209): work out API norms and then add
/// 'pub' to do this.
pub fn decode_json(
    json: &[u8],
    options: JsonOptions,
    value_slot: ValueSlotRef,
) -> Result<(), serde_json_lenient::Error> {
    let mut to_parse = json;
    if to_parse.len() >= 3 && to_parse[0..3] == UTF8_BOM {
        to_parse = &to_parse[3..];
    }
    let mut deserializer = serde_json_lenient::Deserializer::new(SliceRead::new(
        &to_parse,
        options.replace_invalid_characters,
        options.allow_control_chars,
        options.allow_vert_tab,
        options.allow_x_escapes,
    ));
    // By default serde_json[rc] has a recursion limit of 128.
    // As we want different recursion limits in different circumstances,
    // we disable its own recursion tracking and use our own.
    deserializer.disable_recursion_limit();
    deserializer.set_ignore_trailing_commas(options.allow_trailing_commas);
    deserializer.set_allow_comments(options.allow_comments);
    // The C++ parser starts counting nesting levels from the first item
    // inside the outermost dict. We start counting from the
    // absl::optional<base::Value> and also count the outermost dict,
    // therefore we start with -2 to match C++ behavior.
    let result =
        deserializer.deserialize_any(ValueVisitor::new(value_slot, options.max_depth - 2))?;
    deserializer.end()?;
    Ok(result)
}

/// Decode some JSON into a `base::Value`; for calling by C++.
///
/// See `decode_json` for an equivalent which takes and returns idiomatic Rust
/// types, and a little bit more information about the implementation.
///
/// # Args
///
/// * `json`: a slice of input JSON unsigned characters.
/// * `options`: configuration options for non-standard JSON extensions
/// * `value_slot`:  a space into which to construct a base::Value
/// * `error_line`/`error_column`/`error_message`: populated with details of
///                                                any decode error.
///
/// # Returns
///
/// A Boolean indicating whether the decode succeeded.
fn decode_json_from_cpp(
    json: &[u8],
    options: ffi::JsonOptions,
    value_slot: Pin<&mut ffi::ValueSlot>,
    error_line: &mut i32,
    error_column: &mut i32,
    mut error_message: Pin<&mut CxxString>,
) -> bool {
    let value_slot = ValueSlotRef::from(value_slot);
    match decode_json(json, options, value_slot) {
        Err(err) => {
            *error_line = err.line().try_into().unwrap_or(-1);
            *error_column = err.column().try_into().unwrap_or(-1);
            error_message.as_mut().clear();
            // The following line pulls in a lot of binary bloat, due to all the formatter
            // implementations required to stringify error messages. This error message is used in
            // only a couple of places outside unit tests so we could consider trying
            // to eliminate.
            error_message.as_mut().push_str(&err.to_string());
            false
        }
        Ok(_) => true,
    }
}
