bugsnag/bugsnag-js

View on GitHub
packages/plugin-electron-client-state-persistence/src/bugsnag_electron_client_state_persistence.c

Summary

Maintainability
Test Coverage
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(_WIN32) || defined(__WIN32__) || defined(__WINDOWS__)
#include <io.h>
#else
#include <unistd.h>
#endif

#include "bugsnag_electron_client_state_persistence.h"
#include "deps/parson/parson.h"
#include "deps/tinycthread/tinycthread.h"
#include "crash_handler.h"

typedef struct {
  // Path to the serialized file on disk
  const char *save_file_path;
  // Max breadcrumbs to save
  uint8_t max_crumbs;
  // The cached JSON object
  JSON_Value *data;
  // The serialized version of the cached JSON object
  char *serialized_data;
  // Length of serialized data in bytes
  size_t serialized_data_len;
  // Path to the serialized file on disk
  char *last_run_info_file_path;
  // The cached serialized lastRunInfo JSON object
  char *last_run_info_data;
  // Length of lastRunInfo serialized data in bytes
  size_t last_run_info_data_len;
  // A lock for synchronizing access to the JSON object
  mtx_t lock;
} becsp_context;

// Maximum size for all serialized data
static const int BECSP_SERIALIZED_DATA_LEN = 1024 * 1024;
// Local context for storing cached data
static becsp_context g_context = {0};
// Field constants
static const char *const key_app = "app";
static const char *const key_breadcrumbs = "breadcrumbs";
static const char *const key_context = "context";
static const char *const key_device = "device";
static const char *const key_metadata = "metadata";
static const char *const key_feature_flags = "featureFlags";
static const char *const key_session = "session";
static const char *const key_user = "user";
static const char *const keypath_user_id = "user.id";
static const char *const keypath_user_name = "user.name";
static const char *const keypath_user_email = "user.email";

static void handle_crash(void *context) {
  becsp_persist_to_disk();
  bescp_persist_last_run_info_if_required();
  // Uninstall handlers
  becsp_crash_handler_uninstall();
  // Invoke previous handler
  becsp_crash_handler_continue(context);
}

static void serialize_data() {
  if (g_context.data) {
    // Cache serialization size, removing trailing null byte
    g_context.serialized_data_len = json_serialization_size(g_context.data) - 1;
    // Serialize object to buffer
    json_serialize_to_buffer(g_context.data, g_context.serialized_data,
                             BECSP_SERIALIZED_DATA_LEN);
  }
}

static void context_lock() { mtx_lock(&g_context.lock); }

static void context_unlock() { mtx_unlock(&g_context.lock); }

static JSON_Value *initialize_context(const char *state) {
  const char *object_keys[] = {
    key_metadata,
    key_session,
    key_device,
    key_app,
    key_user
  };

  size_t key_count = sizeof(object_keys) / sizeof(const char *);

  if (state) {
    JSON_Value *state_values = json_parse_string(state);
    if (state_values && json_value_get_type(state_values) == JSONObject) {
      JSON_Object *obj = json_value_get_object(state_values);
      // validate known keys for the correct types
      JSON_Value *context = json_object_get_value(obj, key_context);
      if (context && json_value_get_type(context) != JSONString) {
        json_object_remove(obj, key_context);
      }

      JSON_Value *breadcrumbs = json_object_get_value(obj, key_breadcrumbs);
      if (breadcrumbs && json_value_get_type(breadcrumbs) != JSONArray) {
        json_object_remove(obj, key_breadcrumbs);
      }

      JSON_Value *feature_flags = json_object_get_value(obj, key_feature_flags);
      if (feature_flags && json_value_get_type(feature_flags) != JSONArray) {
        json_object_remove(obj, key_feature_flags);
      }

      for (size_t index = 0; index < key_count; index++) {
        const char *key = object_keys[index];
        JSON_Value *value = json_object_get_value(obj, key);
        if (value && json_value_get_type(value) != JSONObject) {
          json_object_remove(obj, key);
        }
      }
      return state_values;
    }
  }
  return json_value_init_object();
}

void becsp_install(const char *save_file_path,
                  const char *last_run_info_file_path,
                  uint8_t max_crumbs,
                  const char *state) {
  if (g_context.data != NULL) {
    return;
  }
  // Initialize the locking mechanism
  mtx_init(&g_context.lock, mtx_plain);
  // Cache the save path
  g_context.save_file_path = strdup(save_file_path);
  // Cache the lastRunInfo save path
  g_context.last_run_info_file_path = strdup(last_run_info_file_path);
  // Set breadcrumb limit
  g_context.max_crumbs = max_crumbs;

  // Create the initial JSON object for storing cached metadata/breadcrumbs
  g_context.data = initialize_context(state);

  // Allocate a buffer for the serialized JSON string
  g_context.serialized_data = calloc(BECSP_SERIALIZED_DATA_LEN, sizeof(char));
  // Cache the empty objects as a JSON string
  serialize_data();
  // Install crash handler
  becsp_crash_handler_install(handle_crash);
}

void becsp_uninstall() {
  if (!g_context.data) {
    return;
  }
  becsp_crash_handler_uninstall();
  free((void *)g_context.save_file_path);
  free((void *)g_context.last_run_info_file_path);
  if(g_context.last_run_info_data_len) {
    free((void *)g_context.last_run_info_data);
  }
  free(g_context.serialized_data);
  json_value_free(g_context.data);

  g_context.serialized_data_len = 0;
  g_context.data = NULL;
  g_context.save_file_path = NULL;
  g_context.serialized_data = NULL;
  g_context.last_run_info_data_len = 0;
  g_context.last_run_info_data = NULL;
  g_context.last_run_info_file_path = NULL;
}

BECSP_STATUS becsp_add_breadcrumb(const char *val) {
  if (!g_context.data) {
    return BECSP_STATUS_NOT_INSTALLED;
  }
  context_lock();
  BECSP_STATUS status = BECSP_STATUS_SUCCESS;

  JSON_Object *obj = json_value_get_object(g_context.data);
  JSON_Value *breadcrumb = json_parse_string(val);
  if (!breadcrumb) {
    status = BECSP_STATUS_INVALID_JSON;
  } else if (json_value_get_type(breadcrumb) != JSONObject) {
    status = BECSP_STATUS_EXPECTED_JSON_OBJECT;
    json_value_free(breadcrumb);
  } else {
    JSON_Value *breadcrumbs_value = json_object_get_value(obj, key_breadcrumbs);
    if (!breadcrumbs_value ||
        json_value_get_type(breadcrumbs_value) != JSONArray) {
      // Initialize the breadcrumb array if not yet present or is an invalid
      // type
      json_object_set_value(obj, key_breadcrumbs, json_value_init_array());
    }

    JSON_Array *breadcrumbs = json_value_get_array(breadcrumbs_value);
    json_array_append_value(breadcrumbs, breadcrumb);
    while (json_array_get_count(breadcrumbs) > g_context.max_crumbs) {
      json_array_remove(breadcrumbs, 0);
    }
    serialize_data();
  }

  context_unlock();
  return status;
}

BECSP_STATUS becsp_set_context(const char *context) {
  if (!g_context.data) {
    return BECSP_STATUS_NOT_INSTALLED;
  }
  context_lock();

  JSON_Object *obj = json_value_get_object(g_context.data);
  if (context) {
    json_object_set_string(obj, key_context, context);
  } else {
    json_object_remove(obj, key_context);
  }

  serialize_data();
  context_unlock();
  return BECSP_STATUS_SUCCESS;
}

BECSP_STATUS becsp_set_metadata(const char *values) {
  if (!g_context.data) {
    return BECSP_STATUS_NOT_INSTALLED;
  }

  context_lock();
  BECSP_STATUS status = BECSP_STATUS_SUCCESS;
  JSON_Object *obj = json_value_get_object(g_context.data);

  if (values) {
    JSON_Value *metadata = json_parse_string(values);
    if (metadata) {
      if (json_value_get_type(metadata) == JSONObject) {
        json_object_set_value(obj, key_metadata, metadata);
      } else {
        status = BECSP_STATUS_EXPECTED_JSON_OBJECT;
        json_value_free(metadata);
      }
    } else {
      status = BECSP_STATUS_INVALID_JSON;
    }
  } else {
    json_object_remove(obj, key_metadata);
  }

  serialize_data();
  context_unlock();
  return status;
}

BECSP_STATUS becsp_update_metadata(const char *tab, const char *val) {
  if (!g_context.data) {
    return BECSP_STATUS_NOT_INSTALLED;
  }

  if (!tab) {
    return BECSP_STATUS_NULL_PARAM;
  }

  context_lock();
  BECSP_STATUS status = BECSP_STATUS_SUCCESS;

  JSON_Object *obj = json_value_get_object(g_context.data);
  JSON_Value *metadata_value = json_object_get_value(obj, key_metadata);
  // In the case that something has gone wrong, and metadata does not exist
  // or is the wrong type, replace it with an object. The old resource will be
  // freed automatically if needed.
  if (!metadata_value || json_value_get_type(metadata_value) != JSONObject) {
    metadata_value = json_value_init_object();
    json_object_set_value(obj, key_metadata, metadata_value);
  }
  JSON_Object *metadata = json_value_get_object(metadata_value);

  if (val) { // Update the tab contents
    JSON_Value *tab_values = json_parse_string(val);
    if (tab_values) {
      json_object_set_value(metadata, tab, tab_values);
    } else {
      status = BECSP_STATUS_INVALID_JSON;
    }
  } else { // Clear the tab contents
    json_object_remove(metadata, tab);
  }

  serialize_data();
  context_unlock();
  return status;
}

BECSP_STATUS becsp_set_feature_flags(const char *feature_flag_json) {
  if (!g_context.data) {
    return BECSP_STATUS_NOT_INSTALLED;
  }

  if (!feature_flag_json) {
    return BECSP_STATUS_NULL_PARAM;
  }

  context_lock();

  BECSP_STATUS status = BECSP_STATUS_SUCCESS;
  JSON_Object *obj = json_value_get_object(g_context.data);

  // Initialize the feature flag array if it's not present or is an invalid type
  JSON_Value *feature_flag_array = json_object_get_value(obj, key_feature_flags);

  if (!feature_flag_array || json_value_get_type(feature_flag_array) != JSONArray) {
    json_object_set_value(obj, key_feature_flags, json_value_init_array());
  }

  JSON_Value *feature_flags = json_parse_string(feature_flag_json);

  if (feature_flags) {
    if (json_value_get_type(feature_flags) == JSONArray) {
      // replace the existing feature flag array with a new one; we don't need
      // to support partial updates
      json_object_set_value(obj, key_feature_flags, feature_flags);
    } else {
      status = BECSP_STATUS_EXPECTED_JSON_ARRAY;
      json_value_free(feature_flags);
    }
  } else {
    status = BECSP_STATUS_INVALID_JSON;
  }

  serialize_data();
  context_unlock();

  return status;
}

BECSP_STATUS becsp_set_app(const char *value) {
  if (!g_context.data) {
    return BECSP_STATUS_NOT_INSTALLED;
  }
  context_lock();
  BECSP_STATUS status = BECSP_STATUS_SUCCESS;
  JSON_Object *obj = json_value_get_object(g_context.data);
  if (value) {
    JSON_Value *pairs = json_parse_string(value);
    if (!pairs) {
      status = BECSP_STATUS_INVALID_JSON;
    } else if (json_value_get_type(pairs) != JSONObject) {
      status = BECSP_STATUS_EXPECTED_JSON_OBJECT;
      json_value_free(pairs);
    } else {
      json_object_set_value(obj, key_app, pairs);
    }
  } else {
    json_object_remove(obj, key_app);
  }

  serialize_data();
  context_unlock();
  return status;
}

BECSP_STATUS becsp_set_device(const char *value) {
  if (!g_context.data) {
    return BECSP_STATUS_NOT_INSTALLED;
  }
  context_lock();
  BECSP_STATUS status = BECSP_STATUS_SUCCESS;
  JSON_Object *obj = json_value_get_object(g_context.data);
  if (value) {
    JSON_Value *pairs = json_parse_string(value);
    if (!pairs) {
      status = BECSP_STATUS_INVALID_JSON;
    } else if (json_value_get_type(pairs) != JSONObject) {
      status = BECSP_STATUS_EXPECTED_JSON_OBJECT;
      json_value_free(pairs);
    } else {
      json_object_set_value(obj, key_device, pairs);
    }
  } else {
    json_object_remove(obj, key_device);
  }

  serialize_data();
  context_unlock();
  return status;
}

BECSP_STATUS becsp_set_session(const char *value) {
  if (!g_context.data) {
    return BECSP_STATUS_NOT_INSTALLED;
  }
  context_lock();
  BECSP_STATUS status = BECSP_STATUS_SUCCESS;
  JSON_Object *obj = json_value_get_object(g_context.data);
  if (value) {
    JSON_Value *pairs = json_parse_string(value);
    if (!pairs) {
      status = BECSP_STATUS_INVALID_JSON;
    } else if (json_value_get_type(pairs) != JSONObject) {
      status = BECSP_STATUS_EXPECTED_JSON_OBJECT;
      json_value_free(pairs);
    } else {
      json_object_set_value(obj, key_session, pairs);
    }
  } else {
    json_object_remove(obj, key_session);
  }

  serialize_data();
  context_unlock();
  return status;
}

BECSP_STATUS becsp_set_user(const char *id, const char *email, const char *name) {
  if (!g_context.data) {
    return BECSP_STATUS_NOT_INSTALLED;
  }
  context_lock();

  JSON_Object *obj = json_value_get_object(g_context.data);
  if (id) {
    json_object_dotset_string(obj, keypath_user_id, id);
  } else {
    json_object_dotremove(obj, keypath_user_id);
  }
  if (email) {
    json_object_dotset_string(obj, keypath_user_email, email);
  } else {
    json_object_dotremove(obj, keypath_user_email);
  }
  if (name) {
    json_object_dotset_string(obj, keypath_user_name, name);
  } else {
    json_object_dotremove(obj, keypath_user_name);
  }

  serialize_data();
  context_unlock();
  return BECSP_STATUS_SUCCESS;
}

BECSP_STATUS becsp_set_last_run_info(const char *encoded_json) {
  if (!g_context.data) {
    return BECSP_STATUS_NOT_INSTALLED;
  }
  context_lock();

  // release the previously cached lastRunInfo string (if there is one)
  if(g_context.last_run_info_data) {
    g_context.last_run_info_data_len = 0;
    free((void*)g_context.last_run_info_data);
  }

  g_context.last_run_info_data = encoded_json;
  g_context.last_run_info_data_len = strlen(encoded_json);

  context_unlock();
  return BECSP_STATUS_SUCCESS;
}

// Must be async-signal-safe
BECSP_STATUS becsp_persist_to_disk() {
  if (!g_context.save_file_path) {
    return BECSP_STATUS_NOT_INSTALLED;
  }
  // Open save file path
  int fd = open(g_context.save_file_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
  if (fd == -1) {
    return BECSP_STATUS_UNKNOWN_FAILURE;
  }
  // Write serialized_data
  size_t len =
      write(fd, g_context.serialized_data, g_context.serialized_data_len);
  // Close save file path
  close(fd);
  return len == g_context.serialized_data_len ? BECSP_STATUS_SUCCESS
                                              : BECSP_STATUS_UNKNOWN_FAILURE;
}

// Must be async-signal-safe - save the lastRunInfo set for a crash
BECSP_STATUS bescp_persist_last_run_info_if_required() {
  if(!g_context.last_run_info_file_path) {
    return BECSP_STATUS_NOT_INSTALLED;
  }

  if(!g_context.last_run_info_data || g_context.last_run_info_data_len == 0) {
    return BECSP_STATUS_SUCCESS;
  }

  int fd = open(g_context.last_run_info_file_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
  if (fd == -1) {
    return BECSP_STATUS_UNKNOWN_FAILURE;
  }
  // Write last_run_info
  write(fd, g_context.last_run_info_data, g_context.last_run_info_data_len);
  // Close last_run_info file
  close(fd);

  return BECSP_STATUS_SUCCESS;
}