Initial Version

This commit is contained in:
Justin Hammond 2022-11-18 01:39:48 +08:00
parent a0030b48a1
commit cafbe33ad9
22 changed files with 4015 additions and 2 deletions

52
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,52 @@
on:
push:
pull_request:
branches: [master]
permissions:
contents: write
name: Build
jobs:
build:
strategy:
fail-fast: true
matrix:
targets: [esp32, esp32s3]
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: esp-idf build
uses: espressif/esp-idf-ci-action@v1
with:
esp_idf_version: v4.4
target: ${{ matrix.targets }}
path: 'examples/esp_ghota_example'
- name: Rename artifact
run: |
cp build/esp_ghota_example.bin esp_ghota_example-${{ matrix.targets }}.bin
cp build/storage.bin storage-${{ matrix.targets }}.bin
- name: Archive Firmware Files
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.targets }}-firmware
path: "*-${{ matrix.targets }}.bin"
release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download Firmware Files
uses: actions/download-artifact@v2
with:
path: release
- name: Release Firmware
uses: ncipollo/release-action@v1
if: startsWith(github.ref, 'refs/tags/')
with:
artifacts: release/*/*.bin
generateReleaseNotes: true
allowUpdates: true
token: ${{ secrets.GITHUB_TOKEN }}

5
.gitignore vendored
View file

@ -50,3 +50,8 @@ modules.order
Module.symvers Module.symvers
Mkfile.old Mkfile.old
dkms.conf dkms.conf
#idf files
build/
dependencies.lock
sdkconfig

12
CMakeLists.txt Normal file
View file

@ -0,0 +1,12 @@
set(priv_requires "log" "freertos" "esp_http_client" "esp-tls" "esp_https_ota" "app_update")
set(requires "esp_event")
set(srcs "src/esp_ghota.c"
"src/lwjson_debug.c"
"src/lwjson.c"
"src/lwjson_stream.c"
"src/semver.c")
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include"
PRIV_REQUIRES ${priv_requires}
REQUIRES ${requires})

33
Kconfig Normal file
View file

@ -0,0 +1,33 @@
menu "Github OTA Configuration"
config MAX_FILENAME_LEN
int "The Max Size Of Filenames"
default 64
help
The Maximum Size of Filenames stored at the Repository
config MAX_URL_LEN
int "The Max Size Of URLs"
default 128
help
The Maximum Size of URl's to download Firmware Files
config GITHUB_HOSTNAME
string "The Hostname of the Github Repository"
default "api.github.com"
help
The Hostname of the Github API Server
config GITHUB_OWNER
string "The Owner of the Github Repository"
default "Fishwaldo"
help
The Owner of the Github Repository
config GITHUB_REPO
string "The Repository of the Github Repository"
default "esp_ghota"
help
The Repository of the Github Repository
endmenu

153
README.md
View file

@ -1,2 +1,151 @@
# esp_ghota # GITHUB OTA for ESP32 devices
esp32 OTA Component to update firmware from Github Releases
Automate your OTA and CI/CD pipeline with Github Actions to update your ESP32 devices in the field direct from github releases
## Features
* Uses the esp_htps_ota library under the hood to update firmware images
* Can also update spiffs/littlefs/fatfs partitions
* Uses SemVer to compare versions and only update if a newer version is available
* Plays nicely with App rollback and anti-rollback features of the esp-idf bootloader
* Download firmware and partitiion images from the github release page directly
* Supports multiple devices with different firmware images
* Includes a sample Github Actions that builds and releases images when a new tag is pushed
* Updates can be triggered manually, or via a interval timer
* Uses a streaming JSON parser for to reduce memory usage (Github API responses can be huge)
* Supports Private Repositories (Github API token required*)
* Supports Github Enterprise
* Supports Github Personal Access Tokens to overcome Github API Ratelimits
* Sends progress of Updates via the esp_event_loop
Note:
You should be careful with your GitHub PAT and putting it in the source code. I would suggest that you store the PAT in NVS, and the user enters it when running, as otherwise the PAT would be easily extractable from your firmware images.
## Usage
via the Espressif Component Registry:
```bash
idf.py add-dependency Fishwaldo/ghota^1.0.0
```
## Example
After Initilizing Network Access, Start a timer to periodically check for new releases:
```c
ghota_config_t ghconfig = {
.filenamematch = "GithubOTA-esp32.bin", // Glob Pattern to match against the Firmware file
.storagenamematch = "storage-esp32.bin", // Glob Pattern to match against the storage firmware file
.storagepartitionname = "storage", // Update the storage partition
.updateInterval = 60, // Check for updates every 60 minuites
};
ghota_client_handle_t *ghota_client = ghota_init(&ghconfig);
if (ghota_client == NULL) {
ESP_LOGE(TAG, "ghota_client_init failed");
return;
}
esp_event_handler_register(GHOTA_EVENTS, ESP_EVENT_ANY_ID, &ghota_event_callback, ghota_client); // Register a handler to get updates on progress
ESP_ERROR_CHECK(ghota_start_update_timer(ghota_client)); // Start the timer to check for updates
```
Manually Checking for updates:
```c
ghota_config_t ghconfig = {
.filenamematch = "GithubOTA-esp32.bin",
.storagenamematch = "storage-esp32.bin",
.storagepartitionname = "storage",
.updateInterval = 60,
};
ghota_client_handle_t *ghota_client = ghota_init(&ghconfig);
if (ghota_client == NULL) {
ESP_LOGE(TAG, "ghota_client_init failed");
return;
}
esp_event_handler_register(GHOTA_EVENTS, ESP_EVENT_ANY_ID, &ghota_event_callback, ghota_client);
ESP_ERROR_CHECK(ghota_check(ghota_client));
semver_t *cur = ghota_get_current_version(ghota_client);
if (cur) {
ESP_LOGI(TAG, "Current version: %d.%d.%d", cur->major, cur->minor, cur->patch);
semver_free(cur);
}
semver_t *new = ghota_get_latest_version(ghota_client);
if (new) {
ESP_LOGI(TAG, "New version: %d.%d.%d", new->major, new->minor, new->patch);
semver_free(new);
}
ESP_ERROR_CHECK(ghota_update(ghota_client));
ESP_ERROR_CHECK(ghota_free(ghota_client));
```
## Configuration
The following configuration options are available:
* config.filenamematch <- Glob pattern to match against the firmware file from the Github Releases page.
* config.storagenamematch <- Glob pattern to match against the storage file from the Github Releases page.
* config.storagepartitionname <- Name of the storage partition to update (as defined in partitions.csv)
* config.hostname <- Hostname of the Github API (default: api.github.com)
* config.orgname <- Name of the Github User or Organization
* config.reponame <- Name of the Github Repository
* config.updateInterval <- Interval in minutes to check for updates
## Github Actions
The Github Actions included in this repository can be used to build and release firmware images to Github Releases.
This is a good way to automate your CI/CD pipeline, and update your devices in the field.
In this example, we build two variants of the Firmware - on for a ESP32 and one for a ESP32-S3 device
Using the filenamematch and storagenamematch config options, we can match against the correct firmware image for the device.
```yaml
on:
push:
pull_request:
branches: [master]
permissions:
contents: write
name: Build
jobs:
build:
strategy:
fail-fast: true
matrix:
targets: [esp32, esp32s3]
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
with:
submodules: 'recursive'
- name: esp-idf build
uses: espressif/esp-idf-ci-action@v1
with:
esp_idf_version: v4.4
target: ${{ matrix.targets }}
- name: Rename artifact
run: |
cp build/GithubOTA.bin GithubOTA-${{ matrix.targets }}.bin
cp build/storage.bin storage-${{ matrix.targets }}.bin
- name: Archive Firmware Files
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.targets }}-firmware
path: "*-${{ matrix.targets }}.bin"
release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download Firmware Files
uses: actions/download-artifact@v2
with:
path: release
- name: Release Firmware
uses: ncipollo/release-action@v1
if: startsWith(github.ref, 'refs/tags/')
with:
artifacts: release/*/*.bin
generateReleaseNotes: true
allowUpdates: true
token: ${{ secrets.GITHUB_TOKEN }}
```

View file

@ -0,0 +1,6 @@
cmake_minimum_required(VERSION 3.16.0)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(esp_ghota_example)

View file

@ -0,0 +1,2 @@
idf_component_register(SRCS "main.c")
spiffs_create_partition_image(storage ../spiffs FLASH_IN_PROJECT)

View file

@ -0,0 +1,8 @@
## IDF Component Manager Manifest File
dependencies:
Fishwaldo/esp_ghota:
version: '*'
override_path: '../../..'
## Required IDF version
idf:
version: ">=4.4.0"

View file

@ -0,0 +1,292 @@
#include <stdlib.h>
#include <string.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_spiffs.h"
#include "lwip/err.h"
#include <lwip/sys.h>
#include <esp_err.h>
#include <esp_log.h>
#include <esp_ghota.h>
#define EXAMPLE_ESP_WIFI_SSID "wifi"
#define EXAMPLE_ESP_WIFI_PASS "password"
/* FreeRTOS event group to signal when we are connected*/
static EventGroupHandle_t s_wifi_event_group;
/* The event group allows multiple bits for each event, but we only care about two events:
* - we are connected to the AP with an IP
* - we failed to connect after the maximum amount of retries */
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1
static int s_retry_num = 0;
static const char* TAG = "main";
static void event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
if (s_retry_num < 10) {
esp_wifi_connect();
s_retry_num++;
ESP_LOGI(TAG, "retry to connect to the AP");
} else {
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
ESP_LOGI(TAG,"connect to the AP fail");
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
s_retry_num = 0;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
}
void wifi_init_sta(void)
{
s_wifi_event_group = xEventGroupCreate();
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
esp_event_handler_instance_t instance_any_id;
esp_event_handler_instance_t instance_got_ip;
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&event_handler,
NULL,
&instance_any_id));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
IP_EVENT_STA_GOT_IP,
&event_handler,
NULL,
&instance_got_ip));
wifi_config_t wifi_config = {
.sta = {
.ssid = EXAMPLE_ESP_WIFI_SSID,
.password = EXAMPLE_ESP_WIFI_PASS,
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
ESP_ERROR_CHECK(esp_wifi_start() );
ESP_LOGI(TAG, "wifi_init_sta finished.");
/* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
* number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
pdFALSE,
pdFALSE,
portMAX_DELAY);
/* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually
* happened. */
if (bits & WIFI_CONNECTED_BIT) {
ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
} else if (bits & WIFI_FAIL_BIT) {
ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS);
} else {
ESP_LOGE(TAG, "UNEXPECTED EVENT");
}
}
static void read_from_spiffs(void)
{
ESP_LOGI(TAG, "Reading hello.txt");
// Open for reading hello.txt
FILE* f = fopen("/spiffs/test.txt", "r");
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open test.txt");
return;
}
char buf[64];
memset(buf, 0, sizeof(buf));
fread(buf, 1, sizeof(buf), f);
fclose(f);
// Display the read contents from the file
ESP_LOGI(TAG, "Read from test.txt: %s", buf);
}
void mount_spiffs() {
esp_vfs_spiffs_conf_t conf = {
.base_path = "/spiffs",
.partition_label = "storage",
.max_files = 5,
.format_if_mount_failed = false
};
// Use settings defined above to initialize and mount SPIFFS filesystem.
// Note: esp_vfs_spiffs_register is an all-in-one convenience function.
esp_err_t ret = esp_vfs_spiffs_register(&conf);
if (ret != ESP_OK) {
if (ret == ESP_FAIL) {
ESP_LOGE(TAG, "Failed to mount or format filesystem");
} else if (ret == ESP_ERR_NOT_FOUND) {
ESP_LOGE(TAG, "Failed to find SPIFFS partition");
} else {
ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));
}
return;
}
size_t total = 0, used = 0;
ret = esp_spiffs_info("storage", &total, &used);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret));
} else {
ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
}
read_from_spiffs();
}
void unmount_spiffs() {
esp_vfs_spiffs_unregister("storage");
}
static void ghota_event_callback(void* handler_args, esp_event_base_t base, int32_t id, void* event_data) {
ghota_client_handle_t *client = (ghota_client_handle_t *)handler_args;
ESP_LOGI(TAG, "Got Update Callback: %s", ghota_get_event_str(id));
if (id == GHOTA_EVENT_START_STORAGE_UPDATE) {
ESP_LOGI(TAG, "Starting storage update");
/* if we are updating the SPIFF storage we should unmount it */
unmount_spiffs();
} else if (id == GHOTA_EVENT_FINISH_STORAGE_UPDATE) {
ESP_LOGI(TAG, "Ending storage update");
/* after updating we can remount, but typically the device will reboot shortly after recieving this event. */
mount_spiffs();
} else if (id == GHOTA_EVENT_FIRMWARE_UPDATE_PROGRESS) {
/* display some progress with the firmware update */
ESP_LOGI(TAG, "Firmware Update Progress: %d%%", *((int*) event_data));
} else if (id == GHOTA_EVENT_STORAGE_UPDATE_PROGRESS) {
/* display some progress with the spiffs partition update */
ESP_LOGI(TAG, "Storage Update Progress: %d%%", *((int*) event_data));
}
(void)client;
return;
}
void app_main() {
ESP_LOGI(TAG, "Starting");
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
wifi_init_sta();
/* initialize our ghota config */
ghota_config_t ghconfig = {
.filenamematch = "esp_ghota-esp32.bin",
.storagenamematch = "storage-esp32.bin",
.storagepartitionname = "storage",
/* 1 minute as a example, but in production you should pick something larger (remember, Github has ratelimites on the API! )*/
.updateInterval = 1,
};
/* initialize ghota. */
ghota_client_handle_t *ghota_client = ghota_init(&ghconfig);
if (ghota_client == NULL) {
ESP_LOGE(TAG, "ghota_client_init failed");
return;
}
/* register for events relating to the update progress */
esp_event_handler_register(GHOTA_EVENTS, ESP_EVENT_ANY_ID, &ghota_event_callback, ghota_client);
#define DO_BACKGROUND_UPDATE 1
#define DO_FOREGROUND_UPDATE 0
#define DO_MANUAL_CHECK_UPDATE 0
#ifdef DO_BACKGROUND_UPDATE
/* for private repositories or to get more API calls than anonymouse, set a github username and PAT token
* see https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token
* for more information on how to create a PAT token.
*
* Be carefull, as the PAT token will be stored in your firmware etc and can be used to access your github account.
*/
//ESP_ERROR_CHECK(ghota_set_auth(ghota_client, "<Insert GH Username>", "<insert PAT TOKEN>"));
/* start a timer that will automatically check for updates based on the interval specified above */
ESP_ERROR_CHECK(ghota_start_update_timer(ghota_client));
#elif DO_FORGROUND_UPDATE
/* or do a check/update now
* This runs in a new task under freeRTOS, so you can do other things while it is running.
*/
ESP_ERROR_CHECK(ghota_start_update_task(ghota_client));
#elif DO_MANUAL_CHECK_UPDATE
/* Alternatively you can do manual checks
* but note, you probably have to increase the Stack size for the task this runs on
*/
/* Query the Github Release API for the latest release */
ESP_ERROR_CHECK(ghota_check(ghota_client));
/* get the semver version of the currently running firmware */
semver_t *cur = ghota_get_current_version(ghota_client);
if (cur) {
ESP_LOGI(TAG, "Current version: %d.%d.%d", cur->major, cur->minor, cur->patch);
semver_free(cur);
/* get the version of the latest release on Github */
semver_t *new = ghota_get_latest_version(ghota_client);
if (new) {
ESP_LOGI(TAG, "New version: %d.%d.%d", new->major, new->minor, new->patch);
semver_free(new);
}
/* do some comparisions */
if (semver_gt(new, cur) == 1) {
ESP_LOGI(TAG, "New version is greater than current version");
} else if (semver_eq(new, cur) == 1) {
ESP_LOGI(TAG, "New version is equal to current version");
} else {
ESP_LOGI(TAG, "New version is less than current version");
}
/* assuming we have a new version, then do a actual update */
ESP_ERROR_CHECK(ghota_update(ghota_client));
/* if there was a new version installed, the esp will reboot after installation and will not reach this code */
#endif
while (1) {
vTaskDelay(5000 / portTICK_PERIOD_MS);
ESP_LOGI(TAG, "This is where we do other things. Memory Dump Below to see the memory usage");
ESP_LOGI(TAG, "Memory: Free %dKiB Low: %dKiB\n", (int)xPortGetFreeHeapSize()/1024, (int)xPortGetMinimumEverFreeHeapSize()/1024);
}
}

View file

@ -0,0 +1,8 @@
# Name, Type, SubType, Offset, Size, Flags
# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
otadata, data, ota, , 0x2000
ota_0, app, ota_0, , 1M,
ota_1, app, ota_1, , 1M
storage, data, spiffs, , 0xF0000,
1 # Name, Type, SubType, Offset, Size, Flags
2 # Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap
3 nvs, data, nvs, 0x9000, 0x6000,
4 phy_init, data, phy, 0xf000, 0x1000,
5 otadata, data, ota, , 0x2000
6 ota_0, app, ota_0, , 1M,
7 ota_1, app, ota_1, , 1M
8 storage, data, spiffs, , 0xF0000,

View file

@ -0,0 +1,7 @@
CONFIG_GITHUB_OWNER="Fishwaldo"
CONFIG_GITHUB_REPO="esp_ghota"
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192

View file

@ -0,0 +1 @@
Hello From Spiffs!

15
idf_component.yml Normal file
View file

@ -0,0 +1,15 @@
# Version of the component [Required, if not passed during upload]
version: "1.0.0"
# Short description for the project [Recommended]
description: esp32 OTA Component to update firmware from Github Releases
# Github repo or a home [Recommended]
url: https://github.com/Fishwaldo/esp_ghota
# List of dependencies [Optional]
# All dependencies of the component should be published in the same registry.
#dependencies:
# Default namespace is `espressif`
# Declaring dependency as `cool_component` is the same as `espressif/cool_component`
#cool_component: ">1.0.0"
#some_ns/some_component:
# version: "~1.2.7"

157
include/esp_ghota.h Normal file
View file

@ -0,0 +1,157 @@
#ifndef GITHUB_OTA_H
#define GITHUB_OTA_H
#include <esp_err.h>
#include <esp_event.h>
#include "semver.h"
#ifdef __cplusplus
extern "C" {
#endif
ESP_EVENT_DECLARE_BASE(GHOTA_EVENTS);
/**
* @brief Github OTA events
* These events are posted to the event loop to track progress of the OTA process
*/
typedef enum
{
GHOTA_EVENT_START_CHECK = 0x01, /*!< Github OTA check started */
GHOTA_EVENT_UPDATE_AVAILABLE = 0x02, /*!< Github OTA update available */
GHOTA_EVENT_NOUPDATE_AVAILABLE = 0x04, /*!< Github OTA no update available */
GHOTA_EVENT_START_UPDATE = 0x08, /*!< Github OTA update started */
GHOTA_EVENT_FINISH_UPDATE = 0x10, /*!< Github OTA update finished */
GHOTA_EVENT_UPDATE_FAILED = 0x20, /*!< Github OTA update failed */
GHOTA_EVENT_START_STORAGE_UPDATE = 0x40, /*!< Github OTA storage update started. If the storage is mounted, you should unmount it when getting this call */
GHOTA_EVENT_FINISH_STORAGE_UPDATE = 0x80, /*!< Github OTA storage update finished. You can mount the new storage after getting this call if needed */
GHOTA_EVENT_STORAGE_UPDATE_FAILED = 0x100, /*!< Github OTA storage update failed */
GHOTA_EVENT_FIRMWARE_UPDATE_PROGRESS = 0x200, /*!< Github OTA firmware update progress */
GHOTA_EVENT_STORAGE_UPDATE_PROGRESS = 0x400, /*!< Github OTA storage update progress */
GHOTA_EVENT_PENDING_REBOOT = 0x800, /*!< Github OTA pending reboot */
} ghota_event_e;
/**
* @brief Github OTA Configuration
*/
typedef struct ghota_config_t {
char filenamematch[CONFIG_MAX_FILENAME_LEN]; /**< Filename to match against on Github indicating this is a firmware file */
char storagenamematch[CONFIG_MAX_FILENAME_LEN]; /**< Filename to match against on Github indicating this is a storage file */
char storagepartitionname[17]; /**< Name of the storage partition to update */
char *hostname; /**< Hostname of the Github server. Defaults to api.github.com*/
char *orgname; /**< Name of the Github organization */
char *reponame; /**< Name of the Github repository */
uint32_t updateInterval; /**< Interval in Minutes to check for updates if using the ghota_start_update_timer function */
} ghota_config_t;
typedef struct ghota_client_handle_t ghota_client_handle_t;
/**
* @brief Initialize the github ota client
*
*
* @param config [in] Configuration for the github ota client
* @return ghota_client_handle_t* handle to pass to all subsequent calls. If it returns NULL, there is a error in your config
*/
ghota_client_handle_t *ghota_init(ghota_config_t *config);
/**
* @brief Set the Username and Password to access private repositories or get more API calls
*
* Anonymus API calls are limited to 60 per hour. If you want to get more calls, you need to set a username and password.
* Be aware that this will be stored in the flash and can be read by anyone with access to the flash.
* The password should be a Github Personal Access Token and for good security you should limit what it can do
*
* @param handle the handle returned by ghota_init
* @param username the username to authenticate with
* @param password this Github Personal Access Token
* @return esp_err_t ESP_OK if all is good, ESP_FAIL if there is an error
*/
esp_err_t ghota_set_auth(ghota_client_handle_t *handle, const char *username, const char *password);
/**
* @brief Free the ghota client handle and all resources
*
* @param handle the Handle
* @return esp_err_t if there was a error
*/
esp_err_t ghota_free(ghota_client_handle_t *handle);
/**
* @brief Perform a check for updates
*
* This will just check if there is a available update on Github releases with download resources that match your configuration
* for firmware and storage files. If it returns ESP_OK, you can call ghota_get_latest_version to get the version of the latest release
*
* @param handle the ghota_client_handle_t handle
* @return esp_err_t ESP_OK if there is a update available, ESP_FAIL if there is no update available or an error
*/
esp_err_t ghota_check(ghota_client_handle_t *handle);
/**
* @brief Downloads and writes the latest firmware and storage partition (if available)
*
* You should only call this after calling ghota_check and ensuring that there is a update available.
*
* @param handle the ghota_client_handle_t handle
* @return esp_err_t ESP_FAIL if there is a error. If the Update is successful, it will not return, but reboot the device
*/
esp_err_t ghota_update(ghota_client_handle_t *handle);
/**
* @brief Get the currently running version of the firmware
*
* This will return the version of the firmware currently running on your device.
* consult semver.h for functions to compare versions
*
* @param handle the ghota_client_handle_t handle
* @return semver_t the version of the latest release
*/
semver_t *ghota_get_current_version(ghota_client_handle_t *handle);
/**
* @brief Get the version of the latest release on Github. Only valid after calling ghota_check
*
* @param handle the ghota_client_handle_t handle
* @return semver_t* the version of the latest release on Github
*/
semver_t *ghota_get_latest_version(ghota_client_handle_t *handle);
/**
* @brief Start a new Task that will check for updates and update if available
*
* This is equivalent to calling ghota_check and ghota_update if there is a new update available.
* If no update is available, it will not update the device.
*
* Progress can be monitored by registering for the GHOTA_EVENTS events on the Global Event Loop
*
* @param handle ghota_client_handle_t handle
* @return esp_err_t ESP_OK if the task was started, ESP_FAIL if there was an error
*/
esp_err_t ghota_start_update_task(ghota_client_handle_t *handle);
/**
* @brief Install a Timer to automatically check for new updates and update if available
*
* Install a timer that will check for new updates every updateInterval seconds and update if available.
*
* @param handle ghota_client_handle_t handle
* @return esp_err_t ESP_OK if no error, otherwise ESP_FAIL
*/
esp_err_t ghota_start_update_timer(ghota_client_handle_t *handle);
/**
* @brief convience function to return a string representation of events emited by this library
*
* @param event the eventid passed to the event handler
* @return char* a string representing the event
*/
char *ghota_get_event_str(ghota_event_e event);
#ifdef __cplusplus
}
#endif
#endif

105
include/semver.h Normal file
View file

@ -0,0 +1,105 @@
/*
* semver.h
*
* Copyright (c) 2015-2017 Tomas Aparicio
* MIT licensed
*/
#ifndef __SEMVER_H
#define __SEMVER_H
#ifdef __cplusplus
extern "C" {
#endif
#ifndef SEMVER_VERSION
#define SEMVER_VERSION "0.2.0"
#endif
/**
* semver_t struct
*/
typedef struct semver_version_s {
int major;
int minor;
int patch;
char * metadata;
char * prerelease;
} semver_t;
/**
* Set prototypes
*/
int
semver_satisfies (semver_t x, semver_t y, const char *op);
int
semver_satisfies_caret (semver_t x, semver_t y);
int
semver_satisfies_patch (semver_t x, semver_t y);
int
semver_compare (semver_t x, semver_t y);
int
semver_compare_version (semver_t x, semver_t y);
int
semver_compare_prerelease (semver_t x, semver_t y);
int
semver_gt (semver_t x, semver_t y);
int
semver_gte (semver_t x, semver_t y);
int
semver_lt (semver_t x, semver_t y);
int
semver_lte (semver_t x, semver_t y);
int
semver_eq (semver_t x, semver_t y);
int
semver_neq (semver_t x, semver_t y);
int
semver_parse (const char *str, semver_t *ver);
int
semver_parse_version (const char *str, semver_t *ver);
void
semver_render (semver_t *x, char *dest);
int
semver_numeric (semver_t *x);
void
semver_bump (semver_t *x);
void
semver_bump_minor (semver_t *x);
void
semver_bump_patch (semver_t *x);
void
semver_free (semver_t *x);
int
semver_is_valid (const char *s);
int
semver_clean (char *s);
#ifdef __cplusplus
}
#endif
#endif

768
src/esp_ghota.c Normal file
View file

@ -0,0 +1,768 @@
#include <stdlib.h>
#include <fnmatch.h>
#include <libgen.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/event_groups.h>
#include <esp_http_client.h>
#include <esp_tls.h>
#include <esp_crt_bundle.h>
#include <esp_log.h>
#include <esp_app_format.h>
#include <esp_ota_ops.h>
#include <esp_https_ota.h>
#include <esp_event.h>
#include <sdkconfig.h>
#include "esp_ghota.h"
#include "lwjson.h"
static const char *TAG = "GHOTA";
ESP_EVENT_DEFINE_BASE(GHOTA_EVENTS);
typedef struct ghota_client_handle_t {
ghota_config_t config;
char *username;
char *token;
struct {
char tag_name[CONFIG_MAX_FILENAME_LEN];
char name[CONFIG_MAX_FILENAME_LEN];
char url[CONFIG_MAX_URL_LEN];
char storageurl[CONFIG_MAX_URL_LEN];
uint8_t flags;
} result;
struct {
char name[CONFIG_MAX_FILENAME_LEN];
char url[CONFIG_MAX_URL_LEN];
} scratch;
semver_t current_version;
semver_t latest_version;
uint32_t countdown;
TaskHandle_t task_handle;
const esp_partition_t *storage_partition;
} ghota_client_handle_t;
enum release_flags
{
GHOTA_RELEASE_GOT_TAG = 0x01,
GHOTA_RELEASE_GOT_FNAME = 0x02,
GHOTA_RELEASE_GOT_URL = 0x04,
GHOTA_RELEASE_GOT_STORAGE = 0x08,
GHOTA_RELEASE_VALID_ASSET = 0x10,
} release_flags;
SemaphoreHandle_t ghota_lock = NULL;
static void SetFlag(ghota_client_handle_t *handle, enum release_flags flag)
{
handle->result.flags |= flag;
}
static bool GetFlag(ghota_client_handle_t *handle, enum release_flags flag)
{
return handle->result.flags & flag;
}
static void ClearFlag(ghota_client_handle_t *handle, enum release_flags flag)
{
handle->result.flags &= ~flag;
}
ghota_client_handle_t *ghota_init(ghota_config_t *newconfig) {
if (!ghota_lock) {
ghota_lock = xSemaphoreCreateMutex();
}
if (xSemaphoreTake(ghota_lock, pdMS_TO_TICKS(1000)) != pdPASS) {
ESP_LOGE(TAG, "Failed to take lock");
return NULL;
}
ghota_client_handle_t *handle = malloc(sizeof(ghota_client_handle_t));
if (handle == NULL) {
ESP_LOGE(TAG, "Failed to allocate memory for client handle");
xSemaphoreGive(ghota_lock);
return NULL;
}
bzero(handle, sizeof(ghota_client_handle_t));
strncpy(handle->config.filenamematch, newconfig->filenamematch, CONFIG_MAX_FILENAME_LEN);
strncpy(handle->config.storagenamematch, newconfig->storagenamematch, CONFIG_MAX_FILENAME_LEN);
strncpy(handle->config.storagepartitionname, newconfig->storagepartitionname, 17);
if (newconfig->hostname == NULL)
asprintf(&handle->config.hostname, CONFIG_GITHUB_HOSTNAME);
else
asprintf(&handle->config.hostname, newconfig->hostname);
if (newconfig->orgname == NULL)
asprintf(&handle->config.orgname, CONFIG_GITHUB_OWNER);
else
asprintf(&handle->config.orgname, newconfig->orgname);
if (newconfig->reponame == NULL)
asprintf(&handle->config.reponame, CONFIG_GITHUB_REPO);
else
asprintf(&handle->config.reponame, newconfig->reponame);
const esp_app_desc_t *app_desc = esp_ota_get_app_description();
if (semver_parse(app_desc->version, &handle->current_version)) {
ESP_LOGE(TAG, "Failed to parse current version");
ghota_free(handle);
xSemaphoreGive(ghota_lock);
return NULL;
}
handle->result.flags = 0;
// if (newconfig->updateInterval < 60) {
// ESP_LOGE(TAG, "Update interval must be at least 60 Minutes");
// newconfig->updateInterval = 60;
// }
handle->config.updateInterval = newconfig->updateInterval;
handle->task_handle = NULL;
xSemaphoreGive(ghota_lock);
return handle;
}
esp_err_t ghota_free(ghota_client_handle_t *handle) {
if (xSemaphoreTake(ghota_lock, pdMS_TO_TICKS(1000)) != pdPASS) {
ESP_LOGE(TAG, "Failed to take lock");
return ESP_FAIL;
}
free(handle->config.hostname);
free(handle->config.orgname);
free(handle->config.reponame);
if (handle->username)
free(handle->username);
if (handle->token)
free(handle->token);
semver_free(&handle->current_version);
semver_free(&handle->latest_version);
xSemaphoreGive(ghota_lock);
return ESP_OK;
}
esp_err_t ghota_set_auth(ghota_client_handle_t *handle, const char *username, const char *password) {
if (xSemaphoreTake(ghota_lock, pdMS_TO_TICKS(1000)) != pdPASS) {
ESP_LOGE(TAG, "Failed to take lock");
return ESP_FAIL;
}
asprintf(&handle->username, "%s", username);
asprintf(&handle->token, "%s", password);
xSemaphoreGive(ghota_lock);
return ESP_OK;
}
static void lwjson_callback(lwjson_stream_parser_t *jsp, lwjson_stream_type_t type)
{
if (jsp->udata == NULL) {
ESP_LOGE(TAG, "No user data for callback");
return;
}
ghota_client_handle_t *handle = (ghota_client_handle_t *)jsp->udata;
#ifdef DEBUG
ESP_LOGI(TAG, "Lwjson Called: %d %d %d %d", jsp->stack_pos, jsp->stack[jsp->stack_pos - 1].type, type, handle->result.flags);
if (jsp->stack[jsp->stack_pos - 1].type == LWJSON_STREAM_TYPE_KEY)
{ /* We need key to be before */
ESP_LOGI(TAG, "Key: %s", jsp->stack[jsp->stack_pos - 1].meta.name);
}
#endif
/* Get a value corresponsing to "tag_name" key */
if (!GetFlag(handle, GHOTA_RELEASE_GOT_TAG)) {
if (jsp->stack_pos >= 2 /* Number of stack entries must be high */
&& jsp->stack[0].type == LWJSON_STREAM_TYPE_OBJECT /* First must be object */
&& jsp->stack[1].type == LWJSON_STREAM_TYPE_KEY /* We need key to be before */
&& strcasecmp(jsp->stack[1].meta.name, "tag_name") == 0)
{
ESP_LOGD(TAG, "Got '%s' with value '%s'", jsp->stack[1].meta.name, jsp->data.str.buff);
strncpy(handle->result.tag_name, jsp->data.str.buff, CONFIG_MAX_FILENAME_LEN);
SetFlag(handle, GHOTA_RELEASE_GOT_TAG);
}
}
if (!GetFlag(handle, GHOTA_RELEASE_VALID_ASSET) || !GetFlag(handle, GHOTA_RELEASE_GOT_STORAGE)) {
if (jsp->stack_pos == 5
&& jsp->stack[0].type == LWJSON_STREAM_TYPE_OBJECT
&& jsp->stack[1].type == LWJSON_STREAM_TYPE_KEY
&& strcasecmp(jsp->stack[1].meta.name, "assets") == 0
&& jsp->stack[2].type == LWJSON_STREAM_TYPE_ARRAY
&& jsp->stack[3].type == LWJSON_STREAM_TYPE_OBJECT
&& jsp->stack[4].type == LWJSON_STREAM_TYPE_KEY)
{
ESP_LOGD(TAG, "Assets Got key '%s' with value '%s'", jsp->stack[jsp->stack_pos - 1].meta.name, jsp->data.str.buff);
if (strcasecmp(jsp->stack[4].meta.name, "name") == 0)
{
strncpy(handle->scratch.name, jsp->data.str.buff, CONFIG_MAX_FILENAME_LEN);
SetFlag(handle, GHOTA_RELEASE_GOT_FNAME);
ESP_LOGD(TAG, "Got Filename for Asset: %s", handle->scratch.name);
}
if (strcasecmp(jsp->stack[4].meta.name, "url") == 0)
{
strncpy(handle->scratch.url, jsp->data.str.buff, CONFIG_MAX_URL_LEN);
SetFlag(handle, GHOTA_RELEASE_GOT_URL);
ESP_LOGD(TAG, "Got URL for Asset: %s", handle->scratch.url);
}
/* Now test if we got both name an download url */
if (GetFlag(handle, GHOTA_RELEASE_GOT_FNAME) && GetFlag(handle, GHOTA_RELEASE_GOT_URL)) {
ESP_LOGD(TAG, "Testing Firmware filenames %s -> %s - Matching Filename against %s and %s", handle->scratch.name, handle->scratch.url, handle->config.filenamematch, handle->config.storagenamematch);
/* see if the filename matches */
if (!GetFlag(handle, GHOTA_RELEASE_VALID_ASSET) && fnmatch(handle->config.filenamematch, handle->scratch.name, 0) == 0) {
strncpy(handle->result.name, handle->scratch.name, CONFIG_MAX_FILENAME_LEN);
strncpy(handle->result.url, handle->scratch.url, CONFIG_MAX_URL_LEN);
ESP_LOGD(TAG, "Valid Firmware Found: %s - %s", handle->result.name, handle->result.url);
SetFlag(handle, GHOTA_RELEASE_VALID_ASSET);
} else if (!GetFlag(handle, GHOTA_RELEASE_GOT_STORAGE) && fnmatch(handle->config.storagenamematch, handle->scratch.name, 0) == 0) {
strncpy(handle->result.storageurl, handle->scratch.url, CONFIG_MAX_URL_LEN);
ESP_LOGD(TAG, "Valid Storage Asset Found: %s - %s", handle->scratch.name, handle->result.storageurl);
SetFlag(handle, GHOTA_RELEASE_GOT_STORAGE);
} else {
ESP_LOGD(TAG, "Invalid Asset Found: %s", handle->scratch.name);
ClearFlag(handle, GHOTA_RELEASE_GOT_FNAME);
ClearFlag(handle, GHOTA_RELEASE_GOT_URL);
}
}
}
}
}
static esp_err_t _http_event_handler(esp_http_client_event_t *evt)
{
lwjsonr_t res;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch"
switch (evt->event_id) {
case HTTP_EVENT_ON_HEADER:
if (strncasecmp(evt->header_key, "x-ratelimit-remaining", strlen("x-ratelimit-remaining")) == 0) {
int limit = atoi(evt->header_value);
ESP_LOGD(TAG, "Github API Rate Limit Remaining: %d", limit);
if (limit < 10) {
ESP_LOGW(TAG, "Github API Rate Limit Remaining is low: %d", limit);
}
}
break;
case HTTP_EVENT_ON_DATA:
if (!esp_http_client_is_chunked_response(evt->client))
{
char *buf = evt->data;
for (int i = 0; i < evt->data_len; i++)
{
res = lwjson_stream_parse((lwjson_stream_parser_t *)evt->user_data, *buf);
if (!(res == lwjsonOK || res == lwjsonSTREAMDONE || res == lwjsonSTREAMINPROG))
{
ESP_LOGE(TAG, "Lwjson Error: %d", res);
}
buf++;
}
}
break;
case HTTP_EVENT_DISCONNECTED: {
int mbedtls_err = 0;
esp_err_t err = esp_tls_get_and_clear_last_error(evt->data, &mbedtls_err, NULL);
if (err != 0)
{
ESP_LOGE(TAG, "Last esp error code: 0x%x", err);
ESP_LOGE(TAG, "Last mbedtls failure: 0x%x", mbedtls_err);
}
break;
}
}
#pragma GCC diagnostic pop
return ESP_OK;
}
esp_err_t ghota_check(ghota_client_handle_t *handle)
{
if (xSemaphoreTake(ghota_lock, pdMS_TO_TICKS(1000)) != pdPASS) {
ESP_LOGE(TAG, "Failed to get lock");
return ESP_FAIL;
}
ESP_LOGI(TAG, "Checking for new release");
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_START_CHECK, handle, sizeof(ghota_client_handle_t *), portMAX_DELAY));
lwjson_stream_parser_t stream_parser;
lwjsonr_t res;
res = lwjson_stream_init(&stream_parser, lwjson_callback);
if (res != lwjsonOK)
{
ESP_LOGE(TAG, "Failed to initialize JSON parser: %d", res);
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_NOUPDATE_AVAILABLE, handle, sizeof(ghota_client_handle_t *), portMAX_DELAY));
xSemaphoreGive(ghota_lock);
return ESP_FAIL;
}
stream_parser.udata = (void*)handle;
char url[CONFIG_MAX_URL_LEN];
snprintf(url, CONFIG_MAX_URL_LEN, "https://%s/repos/%s/%s/releases/latest", handle->config.hostname, handle->config.orgname, handle->config.reponame);
esp_http_client_config_t httpconfig = {
.url = url,
.crt_bundle_attach = esp_crt_bundle_attach,
.event_handler = _http_event_handler,
.user_data = &stream_parser,
};
if (handle->username) {
ESP_LOGD(TAG, "Using Authenticated Request to %s", url);
httpconfig.username = handle->username;
httpconfig.password = handle->token;
httpconfig.auth_type = HTTP_AUTH_TYPE_BASIC;
}
ESP_LOGI(TAG, "Searching for Firmware from %s", url);
esp_http_client_handle_t client = esp_http_client_init(&httpconfig);
esp_err_t err = esp_http_client_perform(client);
if (err == ESP_OK)
{
ESP_LOGD(TAG, "HTTP GET Status = %d, content_length = %d",
esp_http_client_get_status_code(client),
esp_http_client_get_content_length(client));
}
else
{
ESP_LOGE(TAG, "HTTP GET request failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_NOUPDATE_AVAILABLE, handle, sizeof(ghota_client_handle_t *), portMAX_DELAY));
xSemaphoreGive(ghota_lock);
return ESP_FAIL;
}
if (esp_http_client_get_status_code(client) == 200)
{
if (GetFlag(handle, GHOTA_RELEASE_VALID_ASSET))
{
if (semver_parse(handle->result.tag_name, &handle->latest_version)) {
ESP_LOGE(TAG, "Failed to parse new version");
esp_http_client_cleanup(client);
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_NOUPDATE_AVAILABLE, handle, sizeof(ghota_client_handle_t *), portMAX_DELAY));
xSemaphoreGive(ghota_lock);
return ESP_FAIL;
}
ESP_LOGI(TAG, "Current Version %d.%d.%d", handle->current_version.major, handle->current_version.minor, handle->current_version.patch);
ESP_LOGI(TAG, "New Version %d.%d.%d", handle->latest_version.major, handle->latest_version.minor, handle->latest_version.patch);
ESP_LOGI(TAG, "Asset: %s", handle->result.name);
ESP_LOGI(TAG, "Firmware URL: %s", handle->result.url);
if (strlen(handle->result.storageurl)) {
ESP_LOGI(TAG, "Storage URL: %s", handle->result.storageurl);
}
} else {
ESP_LOGI(TAG, "Asset: No Valid Firmware Assets Found");
esp_http_client_cleanup(client);
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_NOUPDATE_AVAILABLE, handle, sizeof(ghota_client_handle_t *), portMAX_DELAY));
xSemaphoreGive(ghota_lock);
return ESP_FAIL;
}
}
else
{
ESP_LOGW(TAG, "Github Release API Returned: %d", esp_http_client_get_status_code(client));
esp_http_client_cleanup(client);
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_NOUPDATE_AVAILABLE, handle, sizeof(ghota_client_handle_t *), portMAX_DELAY));
xSemaphoreGive(ghota_lock);
return ESP_FAIL;
}
esp_http_client_cleanup(client);
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_UPDATE_AVAILABLE, handle, sizeof(ghota_client_handle_t *), portMAX_DELAY));
xSemaphoreGive(ghota_lock);
return ESP_OK;
}
static esp_err_t validate_image_header(esp_app_desc_t *new_app_info)
{
if (new_app_info == NULL) {
return ESP_ERR_INVALID_ARG;
}
ESP_LOGI(TAG, "New Firmware Details:");
ESP_LOGI(TAG, "Project name: %s", new_app_info->project_name);
ESP_LOGI(TAG, "Firmware version: %s", new_app_info->version);
ESP_LOGI(TAG, "Compiled time: %s %s", new_app_info->date, new_app_info->time);
ESP_LOGI(TAG, "ESP-IDF: %s", new_app_info->idf_ver);
ESP_LOGI(TAG, "SHA256:");
ESP_LOG_BUFFER_HEX(TAG, new_app_info->app_elf_sha256, sizeof(new_app_info->app_elf_sha256));
const esp_partition_t *running = esp_ota_get_running_partition();
ESP_LOGD(TAG, "Current partition %s type %d subtype %d (offset 0x%08x)",
running->label, running->type, running->subtype, running->address);
const esp_partition_t *update = esp_ota_get_next_update_partition(NULL);
ESP_LOGD(TAG, "Update partition %s type %d subtype %d (offset 0x%08x)",
update->label, update->type, update->subtype, update->address);
#ifdef CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK
/**
* Secure version check from firmware image header prevents subsequent download and flash write of
* entire firmware image. However this is optional because it is also taken care in API
* esp_https_ota_finish at the end of OTA update procedure.
*/
const uint32_t hw_sec_version = esp_efuse_read_secure_version();
if (new_app_info->secure_version < hw_sec_version) {
ESP_LOGW(TAG, "New firmware security version is less than eFuse programmed, %d < %d", new_app_info->secure_version, hw_sec_version);
return ESP_FAIL;
}
#endif
return ESP_OK;
}
static esp_err_t http_client_set_header_cb(esp_http_client_handle_t http_client)
{
return esp_http_client_set_header(http_client, "Accept", "application/octet-stream");
}
esp_err_t _http_event_storage_handler(esp_http_client_event_t *evt) {
static int output_pos;
static int last_progress;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch"
switch (evt->event_id) {
case HTTP_EVENT_ON_CONNECTED: {
output_pos = 0;
last_progress = 0;
/* Erase the Partition */
break;
}
case HTTP_EVENT_ON_DATA:
if (!esp_http_client_is_chunked_response(evt->client)) {
ghota_client_handle_t *handle = (ghota_client_handle_t *)evt->user_data;
if (output_pos == 0) {
ESP_LOGD(TAG, "Erasing Partition");
ESP_ERROR_CHECK(esp_partition_erase_range(handle->storage_partition, 0, handle->storage_partition->size));
ESP_LOGD(TAG, "Erasing Complete");
}
ESP_ERROR_CHECK(esp_partition_write(handle->storage_partition, output_pos, evt->data, evt->data_len));
output_pos += evt->data_len;
int progress = 100 * ((float)output_pos / (float)handle->storage_partition->size);
if ((progress % 5 == 0) && (progress != last_progress)) {
ESP_LOGV(TAG, "Storage Firmware Update Progress: %d%%", progress);
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_STORAGE_UPDATE_PROGRESS, &progress, sizeof(progress), portMAX_DELAY));
last_progress = progress;
}
}
break;
case HTTP_EVENT_DISCONNECTED: {
int mbedtls_err = 0;
esp_err_t err = esp_tls_get_and_clear_last_error(evt->data, &mbedtls_err, NULL);
if (err != 0)
{
ESP_LOGE(TAG, "Last esp error code: 0x%x", err);
ESP_LOGE(TAG, "Last mbedtls failure: 0x%x", mbedtls_err);
}
break;
}
}
#pragma GCC diagnostic pop
return ESP_OK;
}
esp_err_t ghota_storage_update(ghota_client_handle_t *handle) {
if (xSemaphoreTake(ghota_lock, pdMS_TO_TICKS(1000)) != pdTRUE) {
ESP_LOGE(TAG, "Failed to take lock");
return ESP_FAIL;
}
if (handle == NULL) {
ESP_LOGE(TAG, "Invalid Handle");
xSemaphoreGive(ghota_lock);
return ESP_ERR_INVALID_ARG;
}
if (!strlen(handle->result.storageurl)) {
ESP_LOGE(TAG, "No Storage URL");
xSemaphoreGive(ghota_lock);
return ESP_FAIL;
}
if (!strlen(handle->config.storagepartitionname)) {
ESP_LOGE(TAG, "No Storage Partition Name");
xSemaphoreGive(ghota_lock);
return ESP_FAIL;
}
handle->storage_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, handle->config.storagepartitionname);
if (handle->storage_partition == NULL) {
ESP_LOGE(TAG, "Storage Partition Not Found");
xSemaphoreGive(ghota_lock);
return ESP_FAIL;
}
ESP_LOGD(TAG, "Storage Partition %s - Type %x Subtype %x Found at %x - size %d", handle->storage_partition->label, handle->storage_partition->type, handle->storage_partition->subtype, handle->storage_partition->address, handle->storage_partition->size);
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_START_STORAGE_UPDATE, NULL, 0, portMAX_DELAY));
/* give time for the system to react, such as unmounting the filesystems etc */
vTaskDelay(pdMS_TO_TICKS(1000));
esp_http_client_config_t config = {
.url = handle->result.storageurl,
.event_handler = _http_event_storage_handler,
.crt_bundle_attach = esp_crt_bundle_attach,
.user_data = handle,
.buffer_size_tx = 2048,
};
if (handle->username) {
ESP_LOGD(TAG, "Using Authenticated Request to %s", config.url);
config.username = handle->username;
config.password = handle->token;
config.auth_type = HTTP_AUTH_TYPE_BASIC;
}
esp_http_client_handle_t client = esp_http_client_init(&config);
ESP_ERROR_CHECK(esp_http_client_set_header(client, "Accept", "application/octet-stream"));
esp_err_t err = esp_http_client_perform(client);
if (err == ESP_OK) {
ESP_LOGD(TAG, "HTTP GET Status = %d, content_length = %d",
esp_http_client_get_status_code(client),
esp_http_client_get_content_length(client));
uint8_t sha256[32] = { 0 };
ESP_ERROR_CHECK(esp_partition_get_sha256(handle->storage_partition, sha256));
ESP_LOG_BUFFER_HEX("New Storage Partition SHA256:", sha256, sizeof(sha256));
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_FINISH_STORAGE_UPDATE, NULL, 0, portMAX_DELAY));
} else {
ESP_LOGE(TAG, "HTTP GET request failed: %s", esp_err_to_name(err));
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_STORAGE_UPDATE_FAILED, NULL, 0, portMAX_DELAY));
}
esp_http_client_cleanup(client);
xSemaphoreGive(ghota_lock);
return ESP_OK;
}
esp_err_t ghota_update(ghota_client_handle_t *handle) {
esp_err_t ota_finish_err = ESP_OK;
if (xSemaphoreTake(ghota_lock, pdMS_TO_TICKS(1000)) != pdTRUE) {
ESP_LOGE(TAG, "Failed to take lock");
return ESP_FAIL;
}
ESP_LOGI(TAG, "Scheduled Check for Firmware Update Starting");
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_START_UPDATE, NULL, 0, portMAX_DELAY));
if (!GetFlag(handle, GHOTA_RELEASE_VALID_ASSET)) {
ESP_LOGE(TAG, "No Valid Release Asset Found");
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_UPDATE_FAILED, NULL, 0, portMAX_DELAY));
xSemaphoreGive(ghota_lock);
return ESP_FAIL;
}
int cmp = semver_compare_version(handle->latest_version, handle->current_version);
if ( cmp != 1) {
ESP_LOGE(TAG, "Current Version is equal or newer than new release");
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_UPDATE_FAILED, NULL, 0, portMAX_DELAY));
xSemaphoreGive(ghota_lock);
return ESP_OK;
}
esp_http_client_config_t httpconfig = {
.url = handle->result.url,
.crt_bundle_attach = esp_crt_bundle_attach,
.keep_alive_enable = true,
.buffer_size_tx = 4096,
};
if (handle->username) {
ESP_LOGD(TAG, "Using Authenticated Request to %s", httpconfig.url);
httpconfig.username = handle->username;
httpconfig.password = handle->token;
httpconfig.auth_type = HTTP_AUTH_TYPE_BASIC;
}
esp_https_ota_config_t ota_config = {
.http_config = &httpconfig,
.http_client_init_cb = http_client_set_header_cb,
};
esp_https_ota_handle_t https_ota_handle = NULL;
esp_err_t err = esp_https_ota_begin(&ota_config, &https_ota_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "ESP HTTPS OTA Begin failed: %d", err);
goto ota_end;
}
esp_app_desc_t app_desc;
err = esp_https_ota_get_img_desc(https_ota_handle, &app_desc);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_https_ota_read_img_desc failed: %d", err);
goto ota_end;
}
err = validate_image_header(&app_desc);
if (err != ESP_OK) {
ESP_LOGE(TAG, "image header verification failed: %d", err);
goto ota_end;
}
int last_progress = -1;
while (1) {
err = esp_https_ota_perform(https_ota_handle);
if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) {
break;
}
int32_t dl = esp_https_ota_get_image_len_read(https_ota_handle);
int32_t size = esp_https_ota_get_image_size(https_ota_handle);
int progress = 100 * ((float)dl / (float)size);
if ((progress % 5 == 0) && (progress != last_progress)) {
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_FIRMWARE_UPDATE_PROGRESS, &progress, sizeof(progress), portMAX_DELAY));
ESP_LOGV(TAG, "Firmware Update Progress: %d%%", progress);
last_progress = progress;
}
}
if (esp_https_ota_is_complete_data_received(https_ota_handle) != true) {
// the OTA image was not completely received and user can customise the response to this situation.
ESP_LOGE(TAG, "Complete data was not received.");
} else {
ota_finish_err = esp_https_ota_finish(https_ota_handle);
if ((err == ESP_OK) && (ota_finish_err == ESP_OK)) {
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_FINISH_UPDATE, NULL, 0, portMAX_DELAY));
if (strlen(handle->result.storageurl)) {
xSemaphoreGive(ghota_lock);
if (ghota_storage_update(handle) == ESP_OK) {
ESP_LOGI(TAG, "Storage Update Successful");
} else {
ESP_LOGE(TAG, "Storage Update Failed");
}
} else {
xSemaphoreGive(ghota_lock);
}
ESP_LOGI(TAG, "ESP_HTTPS_OTA upgrade successful. Rebooting ...");
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_PENDING_REBOOT, NULL, 0, portMAX_DELAY));
vTaskDelay(1000 / portTICK_PERIOD_MS);
esp_restart();
return ESP_OK;
} else {
if (ota_finish_err == ESP_ERR_OTA_VALIDATE_FAILED) {
ESP_LOGE(TAG, "Image validation failed, image is corrupted");
}
ESP_LOGE(TAG, "ESP_HTTPS_OTA upgrade failed 0x%x", ota_finish_err);
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_UPDATE_FAILED, NULL, 0, portMAX_DELAY));
xSemaphoreGive(ghota_lock);
return ESP_FAIL;
}
}
ota_end:
esp_https_ota_abort(https_ota_handle);
ESP_LOGE(TAG, "ESP_HTTPS_OTA upgrade failed");
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_UPDATE_FAILED, NULL, 0, portMAX_DELAY));
xSemaphoreGive(ghota_lock);
return ESP_FAIL;
}
semver_t *ghota_get_current_version(ghota_client_handle_t *handle) {
if (!handle) {
return NULL;
}
semver_t *cur = malloc(sizeof(semver_t));
memcpy(cur, &handle->current_version, sizeof(semver_t));
return cur;
}
semver_t *ghota_get_latest_version(ghota_client_handle_t *handle) {
if (!handle) {
return NULL;
}
if (!GetFlag(handle, GHOTA_RELEASE_VALID_ASSET)) {
return NULL;
}
semver_t *new = malloc(sizeof(semver_t));
memcpy(new, &handle->latest_version, sizeof(semver_t));
return new;
}
static void ghota_task(void *pvParameters) {
ghota_client_handle_t *handle = (ghota_client_handle_t *)pvParameters;
ESP_LOGI(TAG, "Firmware Update Task Starting");
if (handle) {
if (ghota_check(handle) == ESP_OK) {
if (semver_gt(handle->latest_version, handle->current_version) == 1) {
ESP_LOGI(TAG, "New Version Available");
ghota_update(handle);
} else {
ESP_LOGI(TAG, "No New Version Available");
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_NOUPDATE_AVAILABLE, NULL, 0, portMAX_DELAY));
}
} else {
ESP_LOGI(TAG, "No Update Available");
ESP_ERROR_CHECK(esp_event_post(GHOTA_EVENTS, GHOTA_EVENT_NOUPDATE_AVAILABLE, NULL, 0, portMAX_DELAY));
}
}
ESP_LOGI(TAG, "Firmware Update Task Finished");
vTaskDelete(handle->task_handle);
vTaskDelay(pdMS_TO_TICKS(1000));
handle->task_handle = NULL;
}
esp_err_t ghota_start_update_task(ghota_client_handle_t *handle) {
if (!handle) {
return ESP_FAIL;
}
eTaskState state = eInvalid;
TaskHandle_t tmp = xTaskGetHandle("ghota_task");
if (tmp) {
state = eTaskGetState(tmp);
}
if (state == eDeleted || state == eInvalid) {
ESP_LOGD(TAG, "Starting Task to Check for Updates");
if (xTaskCreate(ghota_task, "ghota_task", 6144, handle, 5, &handle->task_handle) != pdPASS) {
ESP_LOGW(TAG, "Failed to Start ghota_task");
return ESP_FAIL;
}
} else {
ESP_LOGW(TAG, "ghota_task Already Running");
return ESP_FAIL;
}
return ESP_OK;
}
static void ghota_timer_callback(TimerHandle_t xTimer) {
ghota_client_handle_t *handle = (ghota_client_handle_t *)pvTimerGetTimerID(xTimer);
if (handle) {
handle->countdown--;
if (handle->countdown == 0) {
handle->countdown = handle->config.updateInterval;
ghota_start_update_task(handle);
}
}
}
esp_err_t ghota_start_update_timer(ghota_client_handle_t *handle) {
if (!handle) {
ESP_LOGE(TAG, "Failed to initialize GHOTA Client");
return ESP_FAIL;
}
handle->countdown = handle->config.updateInterval;
/* run timer every minute */
uint64_t ticks = pdMS_TO_TICKS(1000) * 60;
TimerHandle_t timer = xTimerCreate("ghota_timer", ticks, pdTRUE, (void *)handle, ghota_timer_callback);
if ( timer == NULL) {
ESP_LOGE(TAG, "Failed to create timer");
return ESP_FAIL;
} else {
if (xTimerStart(timer, 0) != pdPASS) {
ESP_LOGE(TAG, "Failed to start timer");
return ESP_FAIL;
} else {
ESP_LOGI(TAG, "Started Update Timer for %d Minutes", handle->config.updateInterval);
}
}
return ESP_OK;
}
char *ghota_get_event_str(ghota_event_e event) {
switch (event) {
case GHOTA_EVENT_START_CHECK:
return "GHOTA_EVENT_START_CHECK";
case GHOTA_EVENT_UPDATE_AVAILABLE:
return "GHOTA_EVENT_UPDATE_AVAILABLE";
case GHOTA_EVENT_NOUPDATE_AVAILABLE:
return "GHOTA_EVENT_NOUPDATE_AVAILABLE";
case GHOTA_EVENT_START_UPDATE:
return "GHOTA_EVENT_START_UPDATE";
case GHOTA_EVENT_FINISH_UPDATE:
return "GHOTA_EVENT_FINISH_UPDATE";
case GHOTA_EVENT_UPDATE_FAILED:
return "GHOTA_EVENT_UPDATE_FAILED";
case GHOTA_EVENT_START_STORAGE_UPDATE:
return "GHOTA_EVENT_START_STORAGE_UPDATE";
case GHOTA_EVENT_FINISH_STORAGE_UPDATE:
return "GHOTA_EVENT_FINISH_STORAGE_UPDATE";
case GHOTA_EVENT_STORAGE_UPDATE_FAILED:
return "GHOTA_EVENT_STORAGE_UPDATE_FAILED";
case GHOTA_EVENT_FIRMWARE_UPDATE_PROGRESS:
return "GHOTA_EVENT_FIRMWARE_UPDATE_PROGRESS";
case GHOTA_EVENT_STORAGE_UPDATE_PROGRESS:
return "GHOTA_EVENT_STORAGE_UPDATE_PROGRESS";
case GHOTA_EVENT_PENDING_REBOOT:
return "GHOTA_EVENT_PENDING_REBOOT";
}
return "Unknown Event";
}

710
src/lwjson.c Normal file
View file

@ -0,0 +1,710 @@
/**
* \file lwjson.c
* \brief Lightweight JSON format parser
*/
/*
* Copyright (c) 2022 Tilen MAJERLE
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
* AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* This file is part of LwJSON - Lightweight JSON format parser.
*
* Author: Tilen MAJERLE <tilen@majerle.eu>
* Version: v1.5.0
*/
#include <string.h>
#include "lwjson.h"
/**
* \brief Internal string object
*/
typedef struct {
const char* start; /*!< Original pointer to beginning of JSON object */
size_t len; /*!< Total length of input json string */
const char* p; /*!< Current char pointer */
} lwjson_int_str_t;
/**
* \brief Allocate new token for JSON block
* \param[in] lw: LwJSON instance
* \return Pointer to new token
*/
static lwjson_token_t*
prv_alloc_token(lwjson_t* lw) {
if (lw->next_free_token_pos < lw->tokens_len) {
memset(&lw->tokens[lw->next_free_token_pos], 0x00, sizeof(*lw->tokens));
return &lw->tokens[lw->next_free_token_pos++];
}
return NULL;
}
/**
* \brief Skip all characters that are considered *blank* as per RFC4627
* \param[in,out] pobj: Pointer to text that is modified on success
* \return \ref lwjsonOK on success, member of \ref lwjsonr_t otherwise
*/
static lwjsonr_t
prv_skip_blank(lwjson_int_str_t* pobj) {
while (pobj->p != NULL && *pobj->p != '\0' && (size_t)(pobj->p - pobj->start) < pobj->len) {
if (*pobj->p == ' ' || *pobj->p == '\t' || *pobj->p == '\r' || *pobj->p == '\n' || *pobj->p == '\f') {
++pobj->p;
#if LWJSON_CFG_COMMENTS
/* Check for comments and remove them */
} else if (*pobj->p == '/') {
++pobj->p;
if (pobj->p != NULL && *pobj->p == '*') {
++pobj->p;
while (pobj->p != NULL && *pobj->p != '\0' && (size_t)(pobj->p - pobj->start) < pobj->len) {
if (*pobj->p == '*') {
++pobj->p;
if (*pobj->p == '/') {
++pobj->p;
break;
}
}
++pobj->p;
}
}
#endif /* LWJSON_CFG_COMMENTS */
} else {
break;
}
}
if (pobj->p != NULL && *pobj->p != '\0' && (size_t)(pobj->p - pobj->start) < pobj->len) {
return lwjsonOK;
}
return lwjsonERRJSON;
}
/**
* \brief Parse JSON string that must start end end with double quotes `"` character
* It just parses length of characters and does not perform any decode operation
* \param[in,out] pobj: Pointer to text that is modified on success
* \param[out] pout: Pointer to pointer to string that is set where string starts
* \param[out] poutlen: Length of string in units of characters is stored here
* \return \ref lwjsonOK on success, member of \ref lwjsonr_t otherwise
*/
static lwjsonr_t
prv_parse_string(lwjson_int_str_t* pobj, const char** pout, size_t* poutlen) {
lwjsonr_t res;
size_t len = 0;
if ((res = prv_skip_blank(pobj)) != lwjsonOK) {
return res;
}
if (*pobj->p++ != '"') {
return lwjsonERRJSON;
}
*pout = pobj->p;
/* Parse string but take care of escape characters */
for (;; ++pobj->p, ++len) {
if (pobj->p == NULL || *pobj->p == '\0' || (size_t)(pobj->p - pobj->start) >= pobj->len) {
return lwjsonERRJSON;
}
/* Check special characters */
if (*pobj->p == '\\') {
++pobj->p;
++len;
switch (*pobj->p) {
case '"':
case '\\':
case '/':
case 'b':
case 'f':
case 'n':
case 'r':
case 't':
break;
case 'u':
++pobj->p;
for (size_t i = 0; i < 4; ++i, ++len) {
if (!((*pobj->p >= '0' && *pobj->p <= '9') || (*pobj->p >= 'a' && *pobj->p <= 'f')
|| (*pobj->p >= 'A' && *pobj->p <= 'F'))) {
return lwjsonERRJSON;
}
if (i < 3) {
++pobj->p;
}
}
break;
default:
return lwjsonERRJSON;
}
} else if (*pobj->p == '"') {
++pobj->p;
break;
}
}
*poutlen = len;
return res;
}
/**
* \brief Parse property name that must comply with JSON string format as in RFC4627
* Property string must be followed by colon character ":"
* \param[in,out] pobj: Pointer to text that is modified on success
* \param[out] t: Token instance to write property name to
* \return \ref lwjsonOK on success, member of \ref lwjsonr_t otherwise
*/
static lwjsonr_t
prv_parse_property_name(lwjson_int_str_t* pobj, lwjson_token_t* t) {
lwjsonr_t res;
/* Parse property string first */
if ((res = prv_parse_string(pobj, &t->token_name, &t->token_name_len)) != lwjsonOK) {
return res;
}
/* Skip any spaces */
if ((res = prv_skip_blank(pobj)) != lwjsonOK) {
return res;
}
/* Must continue with colon */
if (*pobj->p++ != ':') {
return lwjsonERRJSON;
}
/* Skip any spaces */
if ((res = prv_skip_blank(pobj)) != lwjsonOK) {
return res;
}
return lwjsonOK;
}
/**
* \brief Parse number as described in RFC4627
* \param[in,out] pobj: Pointer to text that is modified on success
* \param[out] tout: Pointer to output number format
* \param[out] fout: Pointer to output real-type variable. Used if type is REAL.
* \param[out] iout: Pointer to output int-type variable. Used if type is INT.
* \return \ref lwjsonOK on success, member of \ref lwjsonr_t otherwise
*/
static lwjsonr_t
prv_parse_number(lwjson_int_str_t* pobj, lwjson_type_t* tout, lwjson_real_t* fout, lwjson_int_t* iout) {
lwjsonr_t res;
uint8_t is_minus;
lwjson_real_t num;
lwjson_type_t type = LWJSON_TYPE_NUM_INT;
if ((res = prv_skip_blank(pobj)) != lwjsonOK) {
return res;
}
if (*pobj->p == '\0' || (size_t)(pobj->p - pobj->start) >= pobj->len) {
return lwjsonERRJSON;
}
is_minus = *pobj->p == '-' ? (++pobj->p, 1) : 0;
if (*pobj->p == '\0' /* Invalid string */
|| *pobj->p < '0' || *pobj->p > '9' /* Character outside number range */
|| (*pobj->p == '0'
&& (pobj->p[1] < '0' && pobj->p[1] > '9'))) { /* Number starts with 0 but not followed by dot */
return lwjsonERRJSON;
}
/* Parse number */
for (num = 0; *pobj->p >= '0' && *pobj->p <= '9'; ++pobj->p) {
num = num * 10 + (*pobj->p - '0');
}
if (pobj->p != NULL && *pobj->p == '.') { /* Number has exponent */
lwjson_real_t exp, dec_num;
type = LWJSON_TYPE_NUM_REAL; /* Format is real */
++pobj->p; /* Ignore comma character */
if (*pobj->p < '0' || *pobj->p > '9') { /* Must be followed by number characters */
return lwjsonERRJSON;
}
/* Get number after decimal point */
for (exp = 1, dec_num = 0; *pobj->p >= '0' && *pobj->p <= '9'; ++pobj->p, exp *= 10) {
dec_num = dec_num * 10 + (*pobj->p - '0');
}
num += dec_num / exp; /* Add decimal part to number */
}
if (pobj->p != NULL && (*pobj->p == 'e' || *pobj->p == 'E')) { /* Engineering mode */
uint8_t is_minus_exp;
int exp_cnt;
type = LWJSON_TYPE_NUM_REAL; /* Format is real */
++pobj->p; /* Ignore enginnering sing part */
is_minus_exp = *pobj->p == '-' ? (++pobj->p, 1) : 0; /* Check if negative */
if (*pobj->p == '+') { /* Optional '+' is possible too */
++pobj->p;
}
if (*pobj->p < '0' || *pobj->p > '9') { /* Must be followed by number characters */
return lwjsonERRJSON;
}
/* Parse exponent number */
for (exp_cnt = 0; *pobj->p >= '0' && *pobj->p <= '9'; ++pobj->p) {
exp_cnt = exp_cnt * 10 + (*pobj->p - '0');
}
/* Calculate new value for exponent 10^exponent */
if (is_minus_exp) {
for (; exp_cnt > 0; num /= 10, --exp_cnt) {}
} else {
for (; exp_cnt > 0; num *= 10, --exp_cnt) {}
}
}
if (is_minus) {
num = -num;
}
/* Write output values */
if (tout != NULL) {
*tout = type;
}
if (type == LWJSON_TYPE_NUM_INT) {
*iout = (lwjson_int_t)num;
} else {
*fout = num;
}
return lwjsonOK;
}
/**
* \brief Create path segment from input path for search operation
* \param[in,out] p: Pointer to pointer to input path. Pointer is modified
* \param[out] opath: Pointer to pointer to write path segment
* \param[out] olen: Pointer to variable to write length of segment
* \param[out] is_last: Pointer to write if this is last segment
* \return `1` on success, `0` otherwise
*/
static uint8_t
prv_create_path_segment(const char** p, const char** opath, size_t* olen, uint8_t* is_last) {
const char* s = *p;
*is_last = 0;
*opath = NULL;
*olen = 0;
/* Check input path */
if (s == NULL || *s == '\0') {
*is_last = 1;
return 0;
}
/*
* Path must be one of:
* - literal text
* - "#" followed by dot "."
*/
if (*s == '#') {
*opath = s;
for (*olen = 0;; ++s, ++(*olen)) {
if (*s == '.') {
++s;
break;
} else if (*s == '\0') {
if (*olen == 1) {
return 0;
} else {
break;
}
}
}
*p = s;
} else {
*opath = s;
for (*olen = 0; *s != '\0' && *s != '.'; ++(*olen), ++s) {}
*p = s + 1;
}
if (*s == '\0') {
*is_last = 1;
}
return 1;
}
/**
* \brief Input recursive function for find operation
* \param[in] parent: Parent token of type \ref LWJSON_TYPE_ARRAY or LWJSON_TYPE_OBJECT
* \param[in] path: Path to search for starting this token further
* \return Found token on success, `NULL` otherwise
*/
static const lwjson_token_t*
prv_find(const lwjson_token_t* parent, const char* path) {
const char* segment;
size_t segment_len;
uint8_t is_last, result;
/* Get path segments */
if ((result = prv_create_path_segment(&path, &segment, &segment_len, &is_last)) != 0) {
/* Check if detected an array request */
if (*segment == '#') {
/* Parent must be array */
if (parent->type != LWJSON_TYPE_ARRAY) {
return NULL;
}
/* Check if index requested */
if (segment_len > 1) {
const lwjson_token_t* t;
size_t index = 0;
/* Parse number */
for (size_t i = 1; i < segment_len; ++i) {
if (segment[i] < '0' || segment[i] > '9') {
return NULL;
} else {
index = index * 10 + (segment[i] - '0');
}
}
/* Start from beginning */
for (t = parent->u.first_child; t != NULL && index > 0; t = t->next, --index) {}
if (t != NULL) {
if (is_last) {
return t;
} else {
return prv_find(t, path);
}
}
return NULL;
}
/* Scan all indexes and get first match */
for (const lwjson_token_t *tmp_t, *t = parent->u.first_child; t != NULL; t = t->next) {
if ((tmp_t = prv_find(t, path)) != NULL) {
return tmp_t;
}
}
} else {
if (parent->type != LWJSON_TYPE_OBJECT) {
return NULL;
}
for (const lwjson_token_t* t = parent->u.first_child; t != NULL; t = t->next) {
if (t->token_name_len == segment_len && !strncmp(t->token_name, segment, segment_len)) {
const lwjson_token_t* tmp_t;
if (is_last) {
return t;
}
if ((tmp_t = prv_find(t, path)) != NULL) {
return tmp_t;
}
}
}
}
}
return NULL;
}
/**
* \brief Check for character after opening bracket of array or object
* \param[in,out] pobj: JSON string
* \param[in] t: Token to check for type
* \return \ref lwjsonOK on success, member of \ref lwjsonr_t otherwise
*/
static inline lwjsonr_t
prv_check_valid_char_after_open_bracket(lwjson_int_str_t* pobj, lwjson_token_t* t) {
lwjsonr_t res;
/* Check next character after object open */
if ((res = prv_skip_blank(pobj)) != lwjsonOK) {
return res;
}
if (*pobj->p == '\0' || (t->type == LWJSON_TYPE_OBJECT && (*pobj->p != '"' && *pobj->p != '}'))
|| (t->type == LWJSON_TYPE_ARRAY
&& (*pobj->p != '"' && *pobj->p != ']' && *pobj->p != '[' && *pobj->p != '{' && *pobj->p != '-'
&& (*pobj->p < '0' || *pobj->p > '9') && *pobj->p != 't' && *pobj->p != 'n' && *pobj->p != 'f'))) {
res = lwjsonERRJSON;
}
return res;
}
/**
* \brief Setup LwJSON instance for parsing JSON strings
* \param[in,out] lw: LwJSON instance
* \param[in] tokens: Pointer to array of tokens used for parsing
* \param[in] tokens_len: Number of tokens
* \return \ref lwjsonOK on success, member of \ref lwjsonr_t otherwise
*/
lwjsonr_t
lwjson_init(lwjson_t* lw, lwjson_token_t* tokens, size_t tokens_len) {
memset(lw, 0x00, sizeof(*lw));
memset(tokens, 0x00, sizeof(*tokens) * tokens_len);
lw->tokens = tokens;
lw->tokens_len = tokens_len;
lw->first_token.type = LWJSON_TYPE_OBJECT;
return lwjsonOK;
}
/**
* \brief Parse JSON data with length parameter
* JSON format must be complete and must comply with RFC4627
* \param[in,out] lw: LwJSON instance
* \param[in] json_data: JSON string to parse
* \param[in] jsonČlen: JSON data length
* \return \ref lwjsonOK on success, member of \ref lwjsonr_t otherwise
*/
lwjsonr_t
lwjson_parse_ex(lwjson_t* lw, const void* json_data, size_t json_len) {
lwjsonr_t res = lwjsonOK;
lwjson_token_t *t, *to;
lwjson_int_str_t pobj = {.start = json_data, .len = json_len, .p = json_data};
/* Check input parameters */
if (lw == NULL || json_data == NULL || json_len == 0) {
res = lwjsonERRPAR;
goto ret;
}
/* set first token */
to = &lw->first_token;
/* values from very beginning */
lw->flags.parsed = 0;
lw->next_free_token_pos = 0;
memset(to, 0x00, sizeof(*to));
/* First parse */
if ((res = prv_skip_blank(&pobj)) != lwjsonOK) {
goto ret;
}
if (*pobj.p == '{') {
to->type = LWJSON_TYPE_OBJECT;
} else if (*pobj.p == '[') {
to->type = LWJSON_TYPE_ARRAY;
} else {
res = lwjsonERRJSON;
goto ret;
}
++pobj.p;
if ((res = prv_check_valid_char_after_open_bracket(&pobj, to)) != lwjsonOK) {
goto ret;
}
/* Process all characters as indicated by input user */
while (pobj.p != NULL && *pobj.p != '\0' && (size_t)(pobj.p - pobj.start) < pobj.len) {
/* Filter out blanks */
if ((res = prv_skip_blank(&pobj)) != lwjsonOK) {
goto ret;
}
if (*pobj.p == ',') {
++pobj.p;
continue;
}
/* Check if end of object or array*/
if (*pobj.p == (to->type == LWJSON_TYPE_OBJECT ? '}' : ']')) {
lwjson_token_t* parent = to->next;
to->next = NULL;
++pobj.p;
/* End of string if to == NULL (no parent), check if properly terminated */
if ((to = parent) == NULL) {
prv_skip_blank(&pobj);
res = (pobj.p == NULL || *pobj.p == '\0' || (size_t)(pobj.p - pobj.start) == pobj.len) ? lwjsonOK
: lwjsonERR;
goto ret;
}
continue;
}
/* Allocate new token */
if ((t = prv_alloc_token(lw)) == NULL) {
res = lwjsonERRMEM;
goto ret;
}
/* If object type is not array, first thing is property that starts with quotes */
if (to->type != LWJSON_TYPE_ARRAY) {
if (*pobj.p != '"') {
res = lwjsonERRJSON;
goto ret;
}
if ((res = prv_parse_property_name(&pobj, t)) != lwjsonOK) {
goto ret;
}
}
/* Add element to linked list */
if (to->u.first_child == NULL) {
to->u.first_child = t;
} else {
lwjson_token_t* c;
for (c = to->u.first_child; c->next != NULL; c = c->next) {}
c->next = t;
}
/* Check next character to process */
switch (*pobj.p) {
case '{':
case '[':
t->type = *pobj.p == '{' ? LWJSON_TYPE_OBJECT : LWJSON_TYPE_ARRAY;
++pobj.p;
if ((res = prv_check_valid_char_after_open_bracket(&pobj, t)) != lwjsonOK) {
goto ret;
}
t->next = to; /* Temporary saved as parent object */
to = t;
break;
case '"':
if ((res = prv_parse_string(&pobj, &t->u.str.token_value, &t->u.str.token_value_len)) == lwjsonOK) {
t->type = LWJSON_TYPE_STRING;
} else {
goto ret;
}
break;
case 't':
/* RFC4627 is lower-case only */
if (strncmp(pobj.p, "true", 4) == 0) {
t->type = LWJSON_TYPE_TRUE;
pobj.p += 4;
} else {
res = lwjsonERRJSON;
goto ret;
}
break;
case 'f':
/* RFC4627 is lower-case only */
if (strncmp(pobj.p, "false", 5) == 0) {
t->type = LWJSON_TYPE_FALSE;
pobj.p += 5;
} else {
res = lwjsonERRJSON;
goto ret;
}
break;
case 'n':
/* RFC4627 is lower-case only */
if (strncmp(pobj.p, "null", 4) == 0) {
t->type = LWJSON_TYPE_NULL;
pobj.p += 4;
} else {
res = lwjsonERRJSON;
goto ret;
}
break;
default:
if (*pobj.p == '-' || (*pobj.p >= '0' && *pobj.p <= '9')) {
if (prv_parse_number(&pobj, &t->type, &t->u.num_real, &t->u.num_int) != lwjsonOK) {
res = lwjsonERRJSON;
goto ret;
}
} else {
res = lwjsonERRJSON;
goto ret;
}
break;
}
/* Below code is used to check characters after valid tokens */
if (t->type == LWJSON_TYPE_ARRAY || t->type == LWJSON_TYPE_OBJECT) {
continue;
}
/*
* Check what are values after the token value
*
* As per RFC4627, every token value may have one or more
* blank characters, followed by one of below options:
* - Comma separator for next token
* - End of array indication
* - End of object indication
*/
if ((res = prv_skip_blank(&pobj)) != lwjsonOK) {
goto ret;
}
/* Check if valid string is availabe after */
if (pobj.p == NULL || *pobj.p == '\0' || (*pobj.p != ',' && *pobj.p != ']' && *pobj.p != '}')) {
res = lwjsonERRJSON;
goto ret;
} else if (*pobj.p == ',') { /* Check to advance to next token immediatey */
++pobj.p;
}
}
if (to != &lw->first_token || (to != NULL && to->next != NULL)) {
res = lwjsonERRJSON;
to = NULL;
}
if (to != NULL) {
if (to->type != LWJSON_TYPE_ARRAY && to->type != LWJSON_TYPE_OBJECT) {
res = lwjsonERRJSON;
}
to->token_name = NULL;
to->token_name_len = 0;
}
ret:
if (res == lwjsonOK) {
lw->flags.parsed = 1;
}
return res;
}
/**
* \brief Parse input JSON format
* JSON format must be complete and must comply with RFC4627
* \param[in,out] lw: LwJSON instance
* \param[in] json_str: JSON string to parse
* \return \ref lwjsonOK on success, member of \ref lwjsonr_t otherwise
*/
lwjsonr_t
lwjson_parse(lwjson_t* lw, const char* json_str) {
return lwjson_parse_ex(lw, json_str, strlen(json_str));
}
/**
* \brief Free token instances (specially used in case of dynamic memory allocation)
* \param[in,out] lw: LwJSON instance
* \return \ref lwjsonOK on success, member of \ref lwjsonr_t otherwise
*/
lwjsonr_t
lwjson_free(lwjson_t* lw) {
memset(lw->tokens, 0x00, sizeof(*lw->tokens) * lw->tokens_len);
lw->flags.parsed = 0;
return lwjsonOK;
}
/**
* \brief Find first match in the given path for JSON entry
* JSON must be valid and parsed with \ref lwjson_parse function
* \param[in] lw: JSON instance with parsed JSON string
* \param[in] path: Path with dot-separated entries to search for the JSON key to return
* \return Pointer to found token on success, `NULL` if token cannot be found
*/
const lwjson_token_t*
lwjson_find(lwjson_t* lw, const char* path) {
if (lw == NULL || !lw->flags.parsed || path == NULL) {
return NULL;
}
return prv_find(lwjson_get_first_token(lw), path);
}
/**
* \brief Find first match in the given path for JSON path
* JSON must be valid and parsed with \ref lwjson_parse function
*
* \param[in] lw: JSON instance with parsed JSON string
* \param[in] token: Root token to start search at.
* Token must be type \ref LWJSON_TYPE_OBJECT or \ref LWJSON_TYPE_ARRAY.
* Set to `NULL` to use root token of LwJSON object
* \param[in] path: path with dot-separated entries to search for JSON key
* \return Pointer to found token on success, `NULL` if token cannot be found
*/
const lwjson_token_t*
lwjson_find_ex(lwjson_t* lw, const lwjson_token_t* token, const char* path) {
if (lw == NULL || !lw->flags.parsed || path == NULL) {
return NULL;
}
if (token == NULL) {
token = lwjson_get_first_token(lw);
}
if (token == NULL || (token->type != LWJSON_TYPE_ARRAY && token->type != LWJSON_TYPE_OBJECT)) {
return NULL;
}
return prv_find(token, path);
}

335
src/lwjson.h Normal file
View file

@ -0,0 +1,335 @@
/**
* \file lwjson.h
* \brief LwJSON - Lightweight JSON format parser
*/
/*
* Copyright (c) 2022 Tilen MAJERLE
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
* AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* This file is part of LwJSON - Lightweight JSON format parser.
*
* Author: Tilen MAJERLE <tilen@majerle.eu>
* Version: v1.5.0
*/
#ifndef LWJSON_HDR_H
#define LWJSON_HDR_H
#include <string.h>
#include <stdint.h>
#include "lwjson_opt.h"
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
/**
* \defgroup LWJSON Lightweight JSON format parser
* \brief LwJSON - Lightweight JSON format parser
* \{
*/
/**
* \brief Get size of statically allocated array
* \param[in] x: Object to get array size of
* \return Number of elements in array
*/
#define LWJSON_ARRAYSIZE(x) (sizeof(x) / sizeof((x)[0]))
/**
* \brief List of supported JSON types
*/
typedef enum {
LWJSON_TYPE_STRING, /*!< String/Text format. Everything that has beginning and ending quote character */
LWJSON_TYPE_NUM_INT, /*!< Number type for integer */
LWJSON_TYPE_NUM_REAL, /*!< Number type for real number */
LWJSON_TYPE_OBJECT, /*!< Object data type */
LWJSON_TYPE_ARRAY, /*!< Array data type */
LWJSON_TYPE_TRUE, /*!< True boolean value */
LWJSON_TYPE_FALSE, /*!< False boolean value */
LWJSON_TYPE_NULL, /*!< Null value */
} lwjson_type_t;
/**
* \brief Real data type
*/
typedef LWJSON_CFG_REAL_TYPE lwjson_real_t;
/**
* \brief Integer data type
*/
typedef LWJSON_CFG_INT_TYPE lwjson_int_t;
/**
* \brief JSON token
*/
typedef struct lwjson_token {
struct lwjson_token* next; /*!< Next token on a list */
lwjson_type_t type; /*!< Token type */
const char* token_name; /*!< Token name (if exists) */
size_t token_name_len; /*!< Length of token name (this is needed to support const input strings to parse) */
union {
struct {
const char* token_value; /*!< Pointer to the beginning of the string */
size_t
token_value_len; /*!< Length of token value (this is needed to support const input strings to parse) */
} str; /*!< String data */
lwjson_real_t num_real; /*!< Real number format */
lwjson_int_t num_int; /*!< Int number format */
struct lwjson_token* first_child; /*!< First children object for object or array type */
} u; /*!< Union with different data types */
} lwjson_token_t;
/**
* \brief JSON result enumeration
*/
typedef enum {
lwjsonOK = 0x00, /*!< Function returns successfully */
lwjsonERR, /*!< Generic error message */
lwjsonERRJSON, /*!< Error JSON format */
lwjsonERRMEM, /*!< Memory error */
lwjsonERRPAR, /*!< Parameter error */
lwjsonSTREAMWAITFIRSTCHAR, /*!< Streaming parser did not yet receive first valid character
indicating start of JSON sequence */
lwjsonSTREAMDONE, /*!< Streaming parser is done,
closing character matched the stream opening one */
lwjsonSTREAMINPROG, /*!< Stream parsing is still in progress */
}
lwjsonr_t;
/**
* \brief LwJSON instance
*/
typedef struct {
lwjson_token_t* tokens; /*!< Pointer to array of tokens */
size_t tokens_len; /*!< Size of all tokens */
size_t next_free_token_pos; /*!< Position of next free token instance */
lwjson_token_t first_token; /*!< First token on a list */
struct {
uint8_t parsed : 1; /*!< Flag indicating JSON parsing has finished successfully */
} flags; /*!< List of flags */
} lwjson_t;
lwjsonr_t lwjson_init(lwjson_t* lw, lwjson_token_t* tokens, size_t tokens_len);
lwjsonr_t lwjson_parse_ex(lwjson_t* lw, const void* json_data, size_t len);
lwjsonr_t lwjson_parse(lwjson_t* lw, const char* json_str);
const lwjson_token_t* lwjson_find(lwjson_t* lw, const char* path);
const lwjson_token_t* lwjson_find_ex(lwjson_t* lw, const lwjson_token_t* token, const char* path);
lwjsonr_t lwjson_free(lwjson_t* lw);
void lwjson_print_token(const lwjson_token_t* token);
void lwjson_print_json(const lwjson_t* lw);
/**
* \brief Object type for streaming parser
*/
typedef enum {
LWJSON_STREAM_TYPE_NONE, /*!< No entry - not used */
LWJSON_STREAM_TYPE_OBJECT, /*!< Object indication */
LWJSON_STREAM_TYPE_OBJECT_END, /*!< Object end indication */
LWJSON_STREAM_TYPE_ARRAY, /*!< Array indication */
LWJSON_STREAM_TYPE_ARRAY_END, /*!< Array end indication */
LWJSON_STREAM_TYPE_KEY, /*!< Key string */
LWJSON_STREAM_TYPE_STRING, /*!< Strin type */
LWJSON_STREAM_TYPE_TRUE, /*!< True primitive */
LWJSON_STREAM_TYPE_FALSE, /*!< False primitive */
LWJSON_STREAM_TYPE_NULL, /*!< Null primitive */
LWJSON_STREAM_TYPE_NUMBER, /*!< Generic number */
} lwjson_stream_type_t;
/**
* \brief Stream parsing stack object
*/
typedef struct {
lwjson_stream_type_t type; /*!< Streaming type - current value */
union {
char name[LWJSON_CFG_STREAM_KEY_MAX_LEN
+ 1]; /*!< Last known key name, used only for \ref LWJSON_STREAM_TYPE_KEY type */
uint16_t index; /*!< Current index when type is an array */
} meta; /*!< Meta information */
} lwjson_stream_stack_t;
typedef enum {
LWJSON_STREAM_STATE_WAITINGFIRSTCHAR = 0x00, /*!< State to wait for very first opening character */
LWJSON_STREAM_STATE_PARSING, /*!< In parsing of the first char state - detecting next character state */
LWJSON_STREAM_STATE_PARSING_STRING, /*!< Parse string primitive */
LWJSON_STREAM_STATE_PARSING_PRIMITIVE, /*!< Parse any primitive that is non-string, either "true", "false", "null" or a number */
} lwjson_stream_state_t;
/* Forward declaration */
struct lwjson_stream_parser;
/**
* \brief Callback function for various events
*
*/
typedef void (*lwjson_stream_parser_callback_fn)(struct lwjson_stream_parser* jsp, lwjson_stream_type_t type);
/**
* \brief LwJSON streaming structure
*/
typedef struct lwjson_stream_parser {
lwjson_stream_stack_t
stack[LWJSON_CFG_STREAM_STACK_SIZE]; /*!< Stack used for parsing. TODO: Add conditional compilation flag */
size_t stack_pos; /*!< Current stack position */
lwjson_stream_state_t parse_state; /*!< Parser state */
lwjson_stream_parser_callback_fn evt_fn; /*!< Event function for user */
/* State */
union {
struct {
char buff[LWJSON_CFG_STREAM_STRING_MAX_LEN
+ 1]; /*!< Buffer to write temporary data. TODO: Size to be variable with define */
size_t buff_pos; /*!< Buffer position for next write (length of bytes in buffer) */
size_t buff_total_pos; /*!< Total buffer position used up to now (in several data chunks) */
uint8_t is_last; /*!< Status indicates if this is the last part of the string */
} str; /*!< String structure. It is only used for keys and string objects.
Use primitive part for all other options */
struct {
char buff[LWJSON_CFG_STREAM_PRIMITIVE_MAX_LEN + 1]; /*!< Temporary write buffer */
size_t buff_pos; /*!< Buffer position for next write */
} prim; /*!< Primitive object. Used for all types, except key or string */
/* Todo: Add other types */
} data; /*!< Data union used to parse various */
char prev_c; /*!< History of characters */
void *udata; /*!< User data */
} lwjson_stream_parser_t;
lwjsonr_t lwjson_stream_init(lwjson_stream_parser_t* jsp, lwjson_stream_parser_callback_fn evt_fn);
lwjsonr_t lwjson_stream_reset(lwjson_stream_parser_t* jsp);
lwjsonr_t lwjson_stream_parse(lwjson_stream_parser_t* jsp, char c);
/**
* \brief Get number of tokens used to parse JSON
* \param[in] lw: Pointer to LwJSON instance
* \return Number of tokens used to parse JSON
*/
#define lwjson_get_tokens_used(lw) (((lw) != NULL) ? ((lw)->next_free_token_pos + 1) : 0)
/**
* \brief Get very first token of LwJSON instance
* \param[in] lw: Pointer to LwJSON instance
* \return Pointer to first token
*/
#define lwjson_get_first_token(lw) (((lw) != NULL) ? (&(lw)->first_token) : NULL)
/**
* \brief Get token value for \ref LWJSON_TYPE_NUM_INT type
* \param[in] token: token with integer type
* \return Int number if type is integer, `0` otherwise
*/
#define lwjson_get_val_int(token) \
((lwjson_int_t)(((token) != NULL && (token)->type == LWJSON_TYPE_NUM_INT) ? (token)->u.num_int : 0))
/**
* \brief Get token value for \ref LWJSON_TYPE_NUM_REAL type
* \param[in] token: token with real type
* \return Real numbeer if type is real, `0` otherwise
*/
#define lwjson_get_val_real(token) \
((lwjson_real_t)(((token) != NULL && (token)->type == LWJSON_TYPE_NUM_REAL) ? (token)->u.num_real : 0))
/**
* \brief Get first child token for \ref LWJSON_TYPE_OBJECT or \ref LWJSON_TYPE_ARRAY types
* \param[in] token: token with integer type
* \return Pointer to first child or `NULL` if parent token is not object or array
*/
#define lwjson_get_first_child(token) \
(const void*)(((token) != NULL && ((token)->type == LWJSON_TYPE_OBJECT || (token)->type == LWJSON_TYPE_ARRAY)) \
? (token)->u.first_child \
: NULL)
/**
* \brief Get string value from JSON token
* \param[in] token: Token with string type
* \param[out] str_len: Pointer to variable holding length of string.
* Set to `NULL` if not used
* \return Pointer to string or `NULL` if invalid token type
*/
static inline const char*
lwjson_get_val_string(const lwjson_token_t* token, size_t* str_len) {
if (token != NULL && token->type == LWJSON_TYPE_STRING) {
if (str_len != NULL) {
*str_len = token->u.str.token_value_len;
}
return token->u.str.token_value;
}
return NULL;
}
/**
* \brief Get length of string for \ref LWJSON_TYPE_STRING token type
* \param[in] token: token with string type
* \return Length of string in units of bytes
*/
#define lwjson_get_val_string_length(token) \
((size_t)(((token) != NULL && (token)->type == LWJSON_TYPE_STRING) ? (token)->u.str.token_value_len : 0))
/**
* \brief Compare string token with user input string for a case-sensitive match
* \param[in] token: Token with string type
* \param[in] str: NULL-terminated string to compare
* \return `1` if equal, `0` otherwise
*/
static inline uint8_t
lwjson_string_compare(const lwjson_token_t* token, const char* str) {
if (token != NULL && token->type == LWJSON_TYPE_STRING) {
return strncmp(token->u.str.token_value, str, token->u.str.token_value_len) == 0;
}
return 0;
}
/**
* \brief Compare string token with user input string for a case-sensitive match
* \param[in] token: Token with string type
* \param[in] str: NULL-terminated string to compare
* \param[in] len: Length of the string in bytes
* \return `1` if equal, `0` otherwise
*/
static inline uint8_t
lwjson_string_compare_n(const lwjson_token_t* token, const char* str, size_t len) {
if (token != NULL && token->type == LWJSON_TYPE_STRING && len <= token->u.str.token_value_len) {
return strncmp(token->u.str.token_value, str, len) == 0;
}
return 0;
}
/**
* \}
*/
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* LWJSON_HDR_H */

134
src/lwjson_debug.c Normal file
View file

@ -0,0 +1,134 @@
/**
* \file lwjson_debug.c
* \brief Debug and print function for tokens
*/
/*
* Copyright (c) 2022 Tilen MAJERLE
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
* AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* This file is part of LwJSON - Lightweight JSON format parser.
*
* Author: Tilen MAJERLE <tilen@majerle.eu>
* Version: v1.5.0
*/
#include <stdio.h>
#include <string.h>
#include "lwjson.h"
/**
* \brief Token print instance
*/
typedef struct {
size_t indent; /*!< Indent level for token print */
} lwjson_token_print_t;
/**
* \brief Print token value
* \param[in] p: Token print instance
* \param[in] token: Token to print
*/
static void
prv_print_token(lwjson_token_print_t* p, const lwjson_token_t* token) {
#define print_indent() printf("%.*s", (int)((p->indent)), "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t");
if (token == NULL) {
return;
}
/* Check if token has a name */
print_indent();
if (token->token_name != NULL) {
printf("\"%.*s\":", (int)token->token_name_len, token->token_name);
}
/* Print different types */
switch (token->type) {
case LWJSON_TYPE_OBJECT:
case LWJSON_TYPE_ARRAY: {
printf("%c", token->type == LWJSON_TYPE_OBJECT ? '{' : '[');
if (token->u.first_child != NULL) {
printf("\n");
++p->indent;
for (const lwjson_token_t* t = lwjson_get_first_child(token); t != NULL; t = t->next) {
prv_print_token(p, t);
}
--p->indent;
print_indent();
}
printf("%c", token->type == LWJSON_TYPE_OBJECT ? '}' : ']');
break;
}
case LWJSON_TYPE_STRING: {
printf("\"%.*s\"", (int)lwjson_get_val_string_length(token), lwjson_get_val_string(token, NULL));
break;
}
case LWJSON_TYPE_NUM_INT: {
printf("%lld", (long long)lwjson_get_val_int(token));
break;
}
case LWJSON_TYPE_NUM_REAL: {
printf("%f", (double)lwjson_get_val_real(token));
break;
}
case LWJSON_TYPE_TRUE: {
printf("true");
break;
}
case LWJSON_TYPE_FALSE: {
printf("false");
break;
}
case LWJSON_TYPE_NULL: {
printf("NULL");
break;
}
default:
break;
}
if (token->next != NULL) {
printf(",");
}
printf("\n");
}
/**
* \brief Prints and outputs token data to the stream output
* \note This function is not re-entrant
* \param[in] token: Token to print
*/
void
lwjson_print_token(const lwjson_token_t* token) {
lwjson_token_print_t p = {0};
prv_print_token(&p, token);
}
/**
* \brief Prints and outputs full parsed LwJSON instance
* \note This function is not re-entrant
* \param[in] lw: LwJSON instance to print
*/
void
lwjson_print_json(const lwjson_t* lw) {
lwjson_token_print_t p = {0};
prv_print_token(&p, lwjson_get_first_token(lw));
}

136
src/lwjson_opt.h Normal file
View file

@ -0,0 +1,136 @@
/**
* \file lwjson_opt.h
* \brief LwJSON options
*/
/*
* Copyright (c) 2022 Tilen MAJERLE
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
* AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* This file is part of LwJSON - Lightweight JSON format parser.
*
* Author: Tilen MAJERLE <tilen@majerle.eu>
* Version: v1.5.0
*/
#ifndef LWJSON_HDR_OPT_H
#define LWJSON_HDR_OPT_H
//#define LWJSON_DEV 1
/* Uncomment to ignore user options (or set macro in compiler flags) */
#define LWJSON_IGNORE_USER_OPTS
/* Include application options */
#ifndef LWJSON_IGNORE_USER_OPTS
#include "lwjson_opts.h"
#endif /* LWJSON_IGNORE_USER_OPTS */
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
/**
* \defgroup LWJSON_OPT Configuration
* \brief LwJSON options
* \{
*/
/**
* \brief Real data type used to parse numbers with floating point number
* \note Data type must be signed, normally `float` or `double`
*
* This is used for numbers in \ref LWJSON_TYPE_NUM_REAL token data type.
*/
#ifndef LWJSON_CFG_REAL_TYPE
#define LWJSON_CFG_REAL_TYPE float
#endif
/**
* \brief Integer type used to parse numbers
* \note Data type must be signed integer
*
* This is used for numbers in \ref LWJSON_TYPE_NUM_INT token data type.
*/
#ifndef LWJSON_CFG_INT_TYPE
#define LWJSON_CFG_INT_TYPE long long
#endif
/**
* \brief Enables `1` or disables `0` support for inline comments
*
* Default set to `0` to be JSON compliant
*/
#ifndef LWJSON_CFG_COMMENTS
#define LWJSON_CFG_COMMENTS 0
#endif
/**
* \defgroup LWJSON_OPT_STREAM JSON stream
* \brief JSON streaming confiuration
* \{
*/
/**
* \brief Max length of token key (object key name) to be available for stack storage
*
*/
#ifndef LWJSON_CFG_STREAM_KEY_MAX_LEN
#define LWJSON_CFG_STREAM_KEY_MAX_LEN 32
#endif
/**
* \brief Max stack size (depth) in units of \ref lwjson_stream_stack_t structure
*
*/
#ifndef LWJSON_CFG_STREAM_STACK_SIZE
#define LWJSON_CFG_STREAM_STACK_SIZE 16
#endif
/**
* \brief Max size of string for single parsing in units of bytes
*
*/
#ifndef LWJSON_CFG_STREAM_STRING_MAX_LEN
#define LWJSON_CFG_STREAM_STRING_MAX_LEN 256
#endif
/**
* \brief Max number of bytes used to parse primitive.
*
* Primitives are all numbers and logical values (null, true, false)
*/
#ifndef LWJSON_CFG_STREAM_PRIMITIVE_MAX_LEN
#define LWJSON_CFG_STREAM_PRIMITIVE_MAX_LEN 32
#endif
/**
* \}
*/
/**
* \}
*/
#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* LWJSON_HDR_OPT_H */

436
src/lwjson_stream.c Normal file
View file

@ -0,0 +1,436 @@
/**
* \file lwjson_stream.c
* \brief Lightweight JSON format parser
*/
/*
* Copyright (c) 2022 Tilen MAJERLE
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge,
* publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
* AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* This file is part of LwJSON - Lightweight JSON format parser.
*
* Author: Tilen MAJERLE <tilen@majerle.eu>
* Version: v1.5.0
*/
#include <string.h>
#include "lwjson.h"
#if defined(LWJSON_DEV)
#include <stdio.h>
#define DEBUG_STRING_PREFIX_SPACES \
" "
#define LWJSON_DEBUG(jsp, ...) \
do { \
if ((jsp) != NULL) { \
printf("%.*s", (int)(4 * (jsp)->stack_pos), DEBUG_STRING_PREFIX_SPACES); \
} \
printf(__VA_ARGS__); \
} while (0)
/* Strings for debug */
static const char* type_strings[] = {
[LWJSON_STREAM_TYPE_NONE] = "none",
[LWJSON_STREAM_TYPE_OBJECT] = "object",
[LWJSON_STREAM_TYPE_OBJECT_END] = "object_end",
[LWJSON_STREAM_TYPE_ARRAY] = "array",
[LWJSON_STREAM_TYPE_ARRAY_END] = "array_end",
[LWJSON_STREAM_TYPE_KEY] = "key",
[LWJSON_STREAM_TYPE_STRING] = "string",
[LWJSON_STREAM_TYPE_TRUE] = "true",
[LWJSON_STREAM_TYPE_FALSE] = "false",
[LWJSON_STREAM_TYPE_NULL] = "null",
[LWJSON_STREAM_TYPE_NUMBER] = "number",
};
#else
#define LWJSON_DEBUG(jsp, ...)
#endif /* defined(LWJSON_DEV) */
/**
* \brief Sends an event to user for further processing
*
*/
#define SEND_EVT(jsp, type) \
if ((jsp) != NULL && (jsp)->evt_fn != NULL) { \
(jsp)->evt_fn((jsp), (type)); \
}
/**
* \brief Check if character is a space character (with extended chars)
* \param[in] c: Character to check
* \return `1` if considered extended space, `0` otherwise
*/
#define prv_is_space_char_ext(c) ((c) == ' ' || (c) == '\t' || (c) == '\r' || (c) == '\n' || (c) == '\f')
/**
* \brief Push "parent" state to the artificial stack
* \param jsp: JSON stream parser instance
* \param type: Stream type to be pushed on stack
* \return `1` on success, `0` otherwise
*/
static uint8_t
prv_stack_push(lwjson_stream_parser_t* jsp, lwjson_stream_type_t type) {
if (jsp->stack_pos < LWJSON_ARRAYSIZE(jsp->stack)) {
jsp->stack[jsp->stack_pos].type = type;
jsp->stack[jsp->stack_pos].meta.index = 0;
LWJSON_DEBUG(jsp, "Pushed to stack: %s\r\n", type_strings[type]);
jsp->stack_pos++;
return 1;
}
return 0;
}
/**
* \brief Pop value from stack (remove it) and return its value
* \param jsp: JSON stream parser instance
* \return Member of \ref lwjson_stream_type_t enumeration
*/
static lwjson_stream_type_t
prv_stack_pop(lwjson_stream_parser_t* jsp) {
if (jsp->stack_pos > 0) {
lwjson_stream_type_t t = jsp->stack[--jsp->stack_pos].type;
jsp->stack[jsp->stack_pos].type = LWJSON_STREAM_TYPE_NONE;
LWJSON_DEBUG(jsp, "Popped from stack: %s\r\n", type_strings[t]);
/* Take care of array to indicate number of entries */
if (jsp->stack_pos > 0 && jsp->stack[jsp->stack_pos - 1].type == LWJSON_STREAM_TYPE_ARRAY) {
jsp->stack[jsp->stack_pos - 1].meta.index++;
}
return t;
}
return LWJSON_STREAM_TYPE_NONE;
}
/**
* \brief Get top type value currently on the stack
* \param jsp: JSON stream parser instance
* \return Member of \ref lwjson_stream_type_t enumeration
*/
static lwjson_stream_type_t
prv_stack_get_top(lwjson_stream_parser_t* jsp) {
if (jsp->stack_pos > 0) {
return jsp->stack[jsp->stack_pos - 1].type;
}
return LWJSON_STREAM_TYPE_NONE;
}
/**
* \brief Initialize LwJSON stream object before parsing takes place
* \param[in,out] jsp: Stream JSON structure
* \return \ref lwjsonOK on success, member of \ref lwjsonr_t otherwise
*/
lwjsonr_t
lwjson_stream_init(lwjson_stream_parser_t* jsp, lwjson_stream_parser_callback_fn evt_fn) {
memset(jsp, 0x00, sizeof(*jsp));
jsp->parse_state = LWJSON_STREAM_STATE_WAITINGFIRSTCHAR;
jsp->evt_fn = evt_fn;
return lwjsonOK;
}
/**
* \brief Reset LwJSON stream structure
*
* \param jsp: LwJSON stream parser
* \return \ref lwjsonOK on success, member of \ref lwjsonr_t otherwise
*/
lwjsonr_t
lwjson_stream_reset(lwjson_stream_parser_t* jsp) {
jsp->parse_state = LWJSON_STREAM_STATE_WAITINGFIRSTCHAR;
jsp->stack_pos = 0;
return lwjsonOK;
}
/**
* \brief Parse JSON string in streaming mode
* \param[in,out] jsp: Stream JSON structure
* \param[in] c: Character to parse
* \return \ref lwjsonOK if parsing is in progress and no hard error detected
* \ref lwjsonSTREAMDONE when valid JSON was detected and stack level reached back `0` level
*/
lwjsonr_t
lwjson_stream_parse(lwjson_stream_parser_t* jsp, char c) {
/* Get first character first */
if (jsp->parse_state == LWJSON_STREAM_STATE_WAITINGFIRSTCHAR && c != '{' && c != '[') {
return lwjsonSTREAMDONE;
}
start_over:
/*
* Determine what to do from parsing state
*/
switch (jsp->parse_state) {
/*
* Waiting for very first valid characters,
* that is used to indicate start of JSON stream
*/
case LWJSON_STREAM_STATE_WAITINGFIRSTCHAR:
case LWJSON_STREAM_STATE_PARSING: {
/* Determine start of object or an array */
if (c == '{' || c == '[') {
/* Reset stack pointer if this character came from waiting for first character */
if (jsp->parse_state == LWJSON_STREAM_STATE_WAITINGFIRSTCHAR) {
jsp->stack_pos = 0;
}
if (!prv_stack_push(jsp, c == '{' ? LWJSON_STREAM_TYPE_OBJECT : LWJSON_STREAM_TYPE_ARRAY)) {
LWJSON_DEBUG(jsp, "Cannot push object/array to stack\r\n");
return lwjsonERRMEM;
}
jsp->parse_state = LWJSON_STREAM_STATE_PARSING;
SEND_EVT(jsp, c == '{' ? LWJSON_STREAM_TYPE_OBJECT : LWJSON_STREAM_TYPE_ARRAY);
/* Determine end of object or an array */
} else if (c == '}' || c == ']') {
lwjson_stream_type_t t = prv_stack_get_top(jsp);
/*
* If it is a key last entry on closing area,
* it is an error - an example: {"key":}
*/
if (t == LWJSON_STREAM_TYPE_KEY) {
LWJSON_DEBUG(jsp, "ERROR - key should not be followed by ] without value for a key\r\n");
return lwjsonERRJSON;
}
/*
* Check if closing character matches stack value
* Avoid cases like: {"key":"value"] or ["v1", "v2", "v3"}
*/
if ((c == '}' && t != LWJSON_STREAM_TYPE_OBJECT) || (c == ']' && t != LWJSON_STREAM_TYPE_ARRAY)) {
LWJSON_DEBUG(jsp, "ERROR - closing character '%c' does not match stack element \"%s\"\r\n", c,
type_strings[t]);
return lwjsonERRJSON;
}
/* Now remove the array or object from stack */
if (prv_stack_pop(jsp) == LWJSON_STREAM_TYPE_NONE) {
return lwjsonERRJSON;
}
/*
* Check if above is a key type
* and remove it too as we finished with processing of potential case.
*
* {"key":{"abc":1}} - remove "key" part
*/
if (prv_stack_get_top(jsp) == LWJSON_STREAM_TYPE_KEY) {
prv_stack_pop(jsp);
}
SEND_EVT(jsp, c == '}' ? LWJSON_STREAM_TYPE_OBJECT_END : LWJSON_STREAM_TYPE_ARRAY_END);
/* If that is the end of JSON */
if (jsp->stack_pos == 0) {
return lwjsonSTREAMDONE;
}
/* Determine start of string - can be key or regular string (in array or after key) */
} else if (c == '"') {
#if defined(LWJSON_DEV)
lwjson_stream_type_t t = prv_stack_get_top(jsp);
if (t == LWJSON_STREAM_TYPE_OBJECT) {
LWJSON_DEBUG(jsp, "Start of string parsing - expected key name in an object\r\n");
} else if (t == LWJSON_STREAM_TYPE_KEY) {
LWJSON_DEBUG(jsp,
"Start of string parsing - string value associated to previous key in an object\r\n");
} else if (t == LWJSON_STREAM_TYPE_ARRAY) {
LWJSON_DEBUG(jsp, "Start of string parsing - string entry in an array\r\n");
}
#endif /* defined(LWJSON_DEV) */
jsp->parse_state = LWJSON_STREAM_STATE_PARSING_STRING;
memset(&jsp->data.str, 0x00, sizeof(jsp->data.str));
/* Check for end of key character */
} else if (c == ':') {
lwjson_stream_type_t t = prv_stack_get_top(jsp);
/*
* Color can only be followed by key on the stack
*
* It is clear JSON error if this is not the case
*/
if (t != LWJSON_STREAM_TYPE_KEY) {
LWJSON_DEBUG(jsp, "Error - wrong ':' character\r\n");
return lwjsonERRJSON;
}
/* Check if this is start of number or "true", "false" or "null" */
} else if (c == '-' || (c >= '0' && c <= '9') || c == 't' || c == 'f' || c == 'n') {
LWJSON_DEBUG(jsp, "Start of primitive parsing parsing - %s, First char: %c\r\n",
(c == '-' || (c >= '0' && c <= '9')) ? "number" : "true,false,null", c);
jsp->parse_state = LWJSON_STREAM_STATE_PARSING_PRIMITIVE;
memset(&jsp->data.prim, 0x00, sizeof(jsp->data.prim));
jsp->data.prim.buff[jsp->data.prim.buff_pos++] = c;
}
break;
}
/*
* Parse any type of string in a sequence
*
* It is used for key or string in an object or an array
*/
case LWJSON_STREAM_STATE_PARSING_STRING: {
lwjson_stream_type_t t = prv_stack_get_top(jsp);
/*
* Quote character may trigger end of string,
* or if backslasled before - it is part of string
*
* TODO: Handle backslash
*/
if (c == '"' && jsp->prev_c != '\\') {
#if defined(LWJSON_DEV)
if (t == LWJSON_STREAM_TYPE_OBJECT) {
LWJSON_DEBUG(jsp, "End of string parsing - object key name: \"%s\"\r\n", jsp->data.str.buff);
} else if (t == LWJSON_STREAM_TYPE_KEY) {
LWJSON_DEBUG(
jsp, "End of string parsing - string value associated to previous key in an object: \"%s\"\r\n",
jsp->data.str.buff);
} else if (t == LWJSON_STREAM_TYPE_ARRAY) {
LWJSON_DEBUG(jsp, "End of string parsing - an array string entry: \"%s\"\r\n", jsp->data.str.buff);
}
#endif /* defined(LWJSON_DEV) */
/* Set is_last to 1 as this is the last part of this string token */
jsp->data.str.is_last = 1;
/*
* When top of stack is object - string is treated as a key
* When top of stack is a key - string is a value for a key - notify user and pop the value for key
* When top of stack is an array - string is one type - notify user and don't do anything
*/
if (t == LWJSON_STREAM_TYPE_OBJECT) {
SEND_EVT(jsp, LWJSON_STREAM_TYPE_KEY);
if (prv_stack_push(jsp, LWJSON_STREAM_TYPE_KEY)) {
size_t len = jsp->data.str.buff_pos;
if (len > (sizeof(jsp->stack[0].meta.name) - 1)) {
len = sizeof(jsp->stack[0].meta.name) - 1;
}
memcpy(jsp->stack[jsp->stack_pos - 1].meta.name, jsp->data.str.buff, len);
jsp->stack[jsp->stack_pos - 1].meta.name[len] = '\0';
} else {
LWJSON_DEBUG(jsp, "Cannot push key to stack\r\n");
return lwjsonERRMEM;
}
} else if (t == LWJSON_STREAM_TYPE_KEY) {
SEND_EVT(jsp, LWJSON_STREAM_TYPE_STRING);
prv_stack_pop(jsp);
/* Next character to wait for is either space or comma or end of object */
} else if (t == LWJSON_STREAM_TYPE_ARRAY) {
SEND_EVT(jsp, LWJSON_STREAM_TYPE_STRING);
jsp->stack[jsp->stack_pos - 1].meta.index++;
}
jsp->parse_state = LWJSON_STREAM_STATE_PARSING;
} else {
/* TODO: Check other backslash elements */
jsp->data.str.buff[jsp->data.str.buff_pos++] = c;
jsp->data.str.buff_total_pos++;
/* Handle buffer "overflow" */
if (jsp->data.str.buff_pos >= (LWJSON_CFG_STREAM_STRING_MAX_LEN - 1)) {
jsp->data.str.buff[jsp->data.str.buff_pos] = '\0';
/*
* - For array or key types - following one is always string
* - For object type - character is key
*/
SEND_EVT(jsp, (t == LWJSON_STREAM_TYPE_KEY || t == LWJSON_STREAM_TYPE_ARRAY)
? LWJSON_STREAM_TYPE_STRING
: LWJSON_STREAM_TYPE_KEY);
jsp->data.str.buff_pos = 0;
}
}
break;
}
/*
* Parse any type of primitive that is not a string.
*
* true, false, null or any number primitive
*/
case LWJSON_STREAM_STATE_PARSING_PRIMITIVE: {
/* Any character except space, comma, or end of array/object are valid */
if (!prv_is_space_char_ext(c) && c != ',' && c != ']' && c != '}') {
if (jsp->data.prim.buff_pos < sizeof(jsp->data.prim.buff) - 1) {
jsp->data.prim.buff[jsp->data.prim.buff_pos++] = c;
}
} else {
lwjson_stream_type_t t = prv_stack_get_top(jsp);
#if defined(LWJSON_DEV)
if (t == LWJSON_STREAM_TYPE_OBJECT) {
/* TODO: Handle error - primitive cannot be just after object */
} else if (t == LWJSON_STREAM_TYPE_KEY) {
LWJSON_DEBUG(
jsp,
"End of primitive parsing - string value associated to previous key in an object: \"%s\"\r\n",
jsp->data.prim.buff);
} else if (t == LWJSON_STREAM_TYPE_ARRAY) {
LWJSON_DEBUG(jsp, "End of primitive parsing - an array string entry: \"%s\"\r\n",
jsp->data.prim.buff);
}
#endif /* defined(LWJSON_DEV) */
/*
* This is the end of primitive parsing
*
* It is assumed that buffer for primitive can handle at least
* true, false, null or all number characters (that being real or int number)
*/
if (jsp->data.prim.buff_pos == 4 && strncmp(jsp->data.prim.buff, "true", 4) == 0) {
LWJSON_DEBUG(jsp, "Primitive parsed as %s\r\n", "true");
SEND_EVT(jsp, LWJSON_STREAM_TYPE_TRUE);
} else if (jsp->data.prim.buff_pos == 4 && strncmp(jsp->data.prim.buff, "null", 4) == 0) {
LWJSON_DEBUG(jsp, "Primitive parsed as %s\r\n", "null");
SEND_EVT(jsp, LWJSON_STREAM_TYPE_NULL);
} else if (jsp->data.prim.buff_pos == 5 && strncmp(jsp->data.prim.buff, "false", 5) == 0) {
LWJSON_DEBUG(jsp, "Primitive parsed as %s\r\n", "false");
SEND_EVT(jsp, LWJSON_STREAM_TYPE_FALSE);
} else if (jsp->data.prim.buff[0] == '-'
|| (jsp->data.prim.buff[0] >= '0' && jsp->data.prim.buff[0] <= '9')) {
LWJSON_DEBUG(jsp, "Primitive parsed - number\r\n");
SEND_EVT(jsp, LWJSON_STREAM_TYPE_NUMBER);
} else {
LWJSON_DEBUG(jsp, "Invalid primitive type. Got: %s\r\n", jsp->data.prim.buff);
}
if (t == LWJSON_STREAM_TYPE_KEY) {
prv_stack_pop(jsp);
} else if (t == LWJSON_STREAM_TYPE_ARRAY) {
jsp->stack[jsp->stack_pos - 1].meta.index++;
}
/*
* Received character is not part of the primitive and must be processed again
*
* Set state to default state and start from beginning
*/
jsp->parse_state = LWJSON_STREAM_STATE_PARSING;
goto start_over;
}
break;
}
/* TODO: Add other case statements */
default:
break;
}
jsp->prev_c = c; /* Save current c as previous for next round */
return lwjsonSTREAMINPROG;
}

642
src/semver.c Normal file
View file

@ -0,0 +1,642 @@
/*
* semver.c
*
* Copyright (c) 2015-2017 Tomas Aparicio
* MIT licensed
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "semver.h"
#define SLICE_SIZE 50
#define DELIMITER "."
#define PR_DELIMITER "-"
#define MT_DELIMITER "+"
#define NUMBERS "0123456789"
#define ALPHA "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
#define DELIMITERS DELIMITER PR_DELIMITER MT_DELIMITER
#define VALID_CHARS NUMBERS ALPHA DELIMITERS
static const size_t MAX_SIZE = sizeof(char) * 255;
static const int MAX_SAFE_INT = (unsigned int) -1 >> 1;
/**
* Define comparison operators, storing the
* ASCII code per each symbol in hexadecimal notation.
*/
enum operators {
SYMBOL_GT = 0x3e,
SYMBOL_LT = 0x3c,
SYMBOL_EQ = 0x3d,
SYMBOL_TF = 0x7e,
SYMBOL_CF = 0x5e
};
/**
* Private helpers
*/
/*
* Remove [begin:len-begin] from str by moving len data from begin+len to begin.
* If len is negative cut out to the end of the string.
*/
static int
strcut (char *str, int begin, int len) {
size_t l;
l = strlen(str);
if((int)l < 0 || (int)l > MAX_SAFE_INT) return -1;
if (len < 0) len = l - begin + 1;
if (begin + len > (int)l) len = l - begin;
memmove(str + begin, str + begin + len, l - len + 1 - begin);
return len;
}
static int
contains (const char c, const char *matrix, size_t len) {
size_t x;
for (x = 0; x < len; x++)
if ((char) matrix[x] == c) return 1;
return 0;
}
static int
has_valid_chars (const char *str, const char *matrix) {
size_t i, len, mlen;
len = strlen(str);
mlen = strlen(matrix);
for (i = 0; i < len; i++)
if (contains(str[i], matrix, mlen) == 0)
return 0;
return 1;
}
static int
binary_comparison (int x, int y) {
if (x == y) return 0;
if (x > y) return 1;
return -1;
}
static int
parse_int (const char *s) {
int valid, num;
valid = has_valid_chars(s, NUMBERS);
if (valid == 0) return -1;
num = strtol(s, NULL, 10);
if (num > MAX_SAFE_INT) return -1;
return num;
}
/*
* Return a string allocated on the heap with the content from sep to end and
* terminate buf at sep.
*/
static char *
parse_slice (char *buf, char sep) {
char *pr, *part;
int plen;
/* Find separator in buf */
pr = strchr(buf, sep);
if (pr == NULL) return NULL;
/* Length from separator to end of buf */
plen = strlen(pr);
/* Copy from buf into new string */
part = (char*)calloc(plen + 1, sizeof(*part));
if (part == NULL) return NULL;
memcpy(part, pr + 1, plen);
/* Null terminate new string */
part[plen] = '\0';
/* Terminate buf where separator was */
*pr = '\0';
return part;
}
/**
* Parses a string as semver expression.
*
* Returns:
*
* `0` - Parsed successfully
* `-1` - In case of error
*/
int
semver_parse (const char *str, semver_t *ver) {
int valid, res;
size_t len;
char *buf;
bzero(ver, sizeof(semver_t));
valid = semver_is_valid(str);
if (!valid) return -1;
if (*str == 'v') str++;
len = strlen(str);
buf = (char*)calloc(len + 1, sizeof(*buf));
if (buf == NULL) return -1;
strcpy(buf, str);
ver->metadata = parse_slice(buf, MT_DELIMITER[0]);
ver->prerelease = parse_slice(buf, PR_DELIMITER[0]);
res = semver_parse_version(buf, ver);
#if DEBUG > 0
printf("[debug] semver.c %s (%s) = %d.%d.%d - %d\n", str, buf, ver->major, ver->minor, ver->patch, res);
if (ver->prerelease) printf("[debug] semver.c prerelease: %s\n", ver->prerelease);
if (ver->metadata) printf("[debug] semver.c metadata: %s\n", ver->metadata);
//printf("[debug] semver.c %s = %d.%d.%d, %s %s\n", str, ver->major, ver->minor, ver->patch, ver->prerelease, ver->metadata);
#endif
free(buf);
return res;
}
/**
* Parses a given string as semver expression.
*
* Returns:
*
* `0` - Parsed successfully
* `-1` - Parse error or invalid
*/
int
semver_parse_version (const char *str, semver_t *ver) {
size_t len;
int index, value;
char *slice, *next, *endptr;
slice = (char *) str;
index = 0;
while (slice != NULL && index++ < 4) {
next = strchr(slice, DELIMITER[0]);
if (next == NULL)
len = strlen(slice);
else
len = next - slice;
if (len > SLICE_SIZE) return -1;
/* Cast to integer and store */
value = strtol(slice, &endptr, 10);
if (endptr != next && *endptr != '\0') return -1;
switch (index) {
case 1: ver->major = value; break;
case 2: ver->minor = value; break;
case 3: ver->patch = value; break;
}
/* Continue with the next slice */
if (next == NULL)
slice = NULL;
else
slice = next + 1;
}
return 0;
}
static int
compare_prerelease (char *x, char *y) {
char *lastx, *lasty, *xptr, *yptr, *endptr;
int xlen, ylen, xisnum, yisnum, xnum, ynum;
int xn, yn, min, res;
if (x == NULL && y == NULL) return 0;
if (y == NULL && x) return -1;
if (x == NULL && y) return 1;
lastx = x;
lasty = y;
xlen = strlen(x);
ylen = strlen(y);
while (1) {
if ((xptr = strchr(lastx, DELIMITER[0])) == NULL)
xptr = x + xlen;
if ((yptr = strchr(lasty, DELIMITER[0])) == NULL)
yptr = y + ylen;
xnum = strtol(lastx, &endptr, 10);
xisnum = endptr == xptr ? 1 : 0;
ynum = strtol(lasty, &endptr, 10);
yisnum = endptr == yptr ? 1 : 0;
if (xisnum && !yisnum) return -1;
if (!xisnum && yisnum) return 1;
if (xisnum && yisnum) {
/* Numerical comparison */
if (xnum != ynum) return xnum < ynum ? -1 : 1;
} else {
/* String comparison */
xn = xptr - lastx;
yn = yptr - lasty;
min = xn < yn ? xn : yn;
if ((res = strncmp(lastx, lasty, min))) return res < 0 ? -1 : 1;
if (xn != yn) return xn < yn ? -1 : 1;
}
lastx = xptr + 1;
lasty = yptr + 1;
if (lastx == x + xlen + 1 && lasty == y + ylen + 1) break;
if (lastx == x + xlen + 1) return -1;
if (lasty == y + ylen + 1) return 1;
}
return 0;
}
int
semver_compare_prerelease (semver_t x, semver_t y) {
return compare_prerelease(x.prerelease, y.prerelease);
}
/**
* Performs a major, minor and patch binary comparison (x, y).
* This function is mostly used internally
*
* Returns:
*
* `0` - If versiona are equal
* `1` - If x is higher than y
* `-1` - If x is lower than y
*/
int
semver_compare_version (semver_t x, semver_t y) {
int res;
if ((res = binary_comparison(x.major, y.major)) == 0) {
if ((res = binary_comparison(x.minor, y.minor)) == 0) {
return binary_comparison(x.patch, y.patch);
}
}
return res;
}
/**
* Compare two semantic versions (x, y).
*
* Returns:
* - `1` if x is higher than y
* - `0` if x is equal to y
* - `-1` if x is lower than y
*/
int
semver_compare (semver_t x, semver_t y) {
int res;
if ((res = semver_compare_version(x, y)) == 0) {
return semver_compare_prerelease(x, y);
}
return res;
}
/**
* Performs a `greater than` comparison
*/
int
semver_gt (semver_t x, semver_t y) {
return semver_compare(x, y) == 1;
}
/**
* Performs a `lower than` comparison
*/
int
semver_lt (semver_t x, semver_t y) {
return semver_compare(x, y) == -1;
}
/**
* Performs a `equality` comparison
*/
int
semver_eq (semver_t x, semver_t y) {
return semver_compare(x, y) == 0;
}
/**
* Performs a `non equal to` comparison
*/
int
semver_neq (semver_t x, semver_t y) {
return semver_compare(x, y) != 0;
}
/**
* Performs a `greater than or equal` comparison
*/
int
semver_gte (semver_t x, semver_t y) {
return semver_compare(x, y) >= 0;
}
/**
* Performs a `lower than or equal` comparison
*/
int
semver_lte (semver_t x, semver_t y) {
return semver_compare(x, y) <= 0;
}
/**
* Checks if version `x` can be satisfied by `y`
* performing a comparison with caret operator.
*
* See: https://docs.npmjs.com/misc/semver#caret-ranges-1-2-3-0-2-5-0-0-4
*
* Returns:
*
* `1` - Can be satisfied
* `0` - Cannot be satisfied
*/
int
semver_satisfies_caret (semver_t x, semver_t y) {
/* Major versions must always match. */
if (x.major == y.major) {
/* If major version is 0, minor versions must match */
if (x.major == 0) {
/* If minor version is 0, patch must match */
if (x.minor == 0){
return (x.minor == y.minor) && (x.patch == y.patch);
}
/* If minor version is not 0, patch must be >= */
else if (x.minor == y.minor){
return x.patch >= y.patch;
}
else{
return 0;
}
}
else if (x.minor > y.minor){
return 1;
}
else if (x.minor == y.minor)
{
return x.patch >= y.patch;
}
else {
return 0;
}
}
return 0;
}
/**
* Checks if version `x` can be satisfied by `y`
* performing a comparison with tilde operator.
*
* See: https://docs.npmjs.com/misc/semver#tilde-ranges-1-2-3-1-2-1
*
* Returns:
*
* `1` - Can be satisfied
* `0` - Cannot be satisfied
*/
int
semver_satisfies_patch (semver_t x, semver_t y) {
return x.major == y.major
&& x.minor == y.minor;
}
/**
* Checks if both versions can be satisfied
* based on the given comparison operator.
*
* Allowed operators:
*
* - `=` - Equality
* - `>=` - Higher or equal to
* - `<=` - Lower or equal to
* - `<` - Lower than
* - `>` - Higher than
* - `^` - Caret comparison (see https://docs.npmjs.com/misc/semver#caret-ranges-1-2-3-0-2-5-0-0-4)
* - `~` - Tilde comparison (see https://docs.npmjs.com/misc/semver#tilde-ranges-1-2-3-1-2-1)
*
* Returns:
*
* `1` - Can be satisfied
* `0` - Cannot be satisfied
*/
int
semver_satisfies (semver_t x, semver_t y, const char *op) {
int first, second;
/* Extract the comparison operator */
first = op[0];
second = op[1];
/* Caret operator */
if (first == SYMBOL_CF)
return semver_satisfies_caret(x, y);
/* Tilde operator */
if (first == SYMBOL_TF)
return semver_satisfies_patch(x, y);
/* Strict equality */
if (first == SYMBOL_EQ)
return semver_eq(x, y);
/* Greater than or equal comparison */
if (first == SYMBOL_GT) {
if (second == SYMBOL_EQ) {
return semver_gte(x, y);
}
return semver_gt(x, y);
}
/* Lower than or equal comparison */
if (first == SYMBOL_LT) {
if (second == SYMBOL_EQ) {
return semver_lte(x, y);
}
return semver_lt(x, y);
}
return 0;
}
/**
* Free heep allocated memory of a given semver.
* This is just a convenient function that you
* should call when you're done.
*/
void
semver_free (semver_t *x) {
if (x->metadata) {
free(x->metadata);
x->metadata = NULL;
}
if (x->prerelease) {
free(x->prerelease);
x->prerelease = NULL;
}
}
/**
* Renders
*/
static void
concat_num (char * str, int x, char * sep) {
char buf[SLICE_SIZE] = {0};
if (sep == NULL) sprintf(buf, "%d", x);
else sprintf(buf, "%s%d", sep, x);
strcat(str, buf);
}
static void
concat_char (char * str, char * x, char * sep) {
char buf[SLICE_SIZE] = {0};
sprintf(buf, "%s%s", sep, x);
strcat(str, buf);
}
/**
* Render a given semver as string
*/
void
semver_render (semver_t *x, char *dest) {
concat_num(dest, x->major, NULL);
concat_num(dest, x->minor, DELIMITER);
concat_num(dest, x->patch, DELIMITER);
if (x->prerelease) concat_char(dest, x->prerelease, PR_DELIMITER);
if (x->metadata) concat_char(dest, x->metadata, MT_DELIMITER);
}
/**
* Version bump helpers
*/
void
semver_bump (semver_t *x) {
x->major++;
}
void
semver_bump_minor (semver_t *x) {
x->minor++;
}
void
semver_bump_patch (semver_t *x) {
x->patch++;
}
/**
* Helpers
*/
static int
has_valid_length (const char *s) {
return strlen(s) <= MAX_SIZE;
}
/**
* Checks if a given semver string is valid
*
* Returns:
*
* `1` - Valid expression
* `0` - Invalid
*/
int
semver_is_valid (const char *s) {
return has_valid_length(s)
&& has_valid_chars(s, VALID_CHARS);
}
/**
* Removes non-valid characters in the given string.
*
* Returns:
*
* `0` - Valid
* `-1` - Invalid input
*/
int
semver_clean (char *s) {
size_t i, len, mlen;
int res;
if (has_valid_length(s) == 0) return -1;
len = strlen(s);
mlen = strlen(VALID_CHARS);
for (i = 0; i < len; i++) {
if (contains(s[i], VALID_CHARS, mlen) == 0) {
res = strcut(s, i, 1);
if(res == -1) return -1;
--len; --i;
}
}
return 0;
}
static int
char_to_int (const char * str) {
int buf;
size_t i,len, mlen;
buf = 0;
len = strlen(str);
mlen = strlen(VALID_CHARS);
for (i = 0; i < len; i++)
if (contains(str[i], VALID_CHARS, mlen))
buf += (int) str[i];
return buf;
}
/**
* Render a given semver as numeric value.
* Useful for ordering and filtering.
*/
int
semver_numeric (semver_t *x) {
int num;
char buf[SLICE_SIZE * 3];
memset(&buf, 0, SLICE_SIZE * 3);
if (x->major) concat_num(buf, x->major, NULL);
if (x->major || x->minor) concat_num(buf, x->minor, NULL);
if (x->major || x->minor || x->patch) concat_num(buf, x->patch, NULL);
num = parse_int(buf);
if(num == -1) return -1;
if (x->prerelease) num += char_to_int(x->prerelease);
if (x->metadata) num += char_to_int(x->metadata);
return num;
}