diff --git a/README.md b/README.md index 5cd7cecf..23a11c83 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,30 @@ # Project -> This repo has been populated by an initial template to help get you started. Please -> make sure to update the content to build a great experience for community-building. +For more details about the Azure Embedded SDK for C please refer to the [official library website](https://github.com/azure/azure-sdk-for-c). -As the maintainer of this project, please make a few updates: +This library package contains the following samples. +Please refer to their documentation for setup and execution instructions. -- Improving this README.MD file to provide a great experience -- Updating SUPPORT.MD with content about this project's support experience -- Understanding the security reporting process in SECURITY.MD -- Remove this section from the README +[ESPRESSIF ESP-8266](examples/Azure_IoT_Hub_ESP8266/readme.md) -## Contributing +[ESPRESSIF ESP-32](examples/Azure_IoT_Hub_ESP32/readme.md) + +[Realtek AmebaD](examples/Azure_IoT_Hub_RealtekAmebaD/readme.md) -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. +## Contributing -When you submit a pull request, a CLA bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. +For reporting any issues or requesting support, please open an issue on [azure-sdk-for-c](https://github.com/Azure/azure-sdk-for-c/issues/new/choose). This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. -## Trademarks +### Reporting Security Issues and Security Bugs + +Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) . You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including the MSRC PGP key, can be found in the [Security TechCenter](https://www.microsoft.com/msrc/faqs-report-an-issue). + +### License + +This Azure SDK for C Arduino library is licensed under [MIT](https://github.com/Azure/azure-sdk-for-c-arduino/blob/main/LICENSE) license. -This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft -trademarks or logos is subject to and must follow -[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). -Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. -Any use of third-party trademarks or logos are subject to those third-party's policies. +Azure SDK for Embedded C is licensed under the [MIT](https://github.com/Azure/azure-sdk-for-c/blob/main/LICENSE) license. diff --git a/examples/Azure_IoT_Hub_ESP32/AzIoTSasToken.cpp b/examples/Azure_IoT_Hub_ESP32/AzIoTSasToken.cpp new file mode 100644 index 00000000..25415392 --- /dev/null +++ b/examples/Azure_IoT_Hub_ESP32/AzIoTSasToken.cpp @@ -0,0 +1,282 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "AzIoTSasToken.h" +#include "SerialLogger.h" +#include +#include +#include +#include +#include +#include + +#define INDEFINITE_TIME ((time_t)-1) + +#define az_span_is_empty(x) (az_span_size(x) == az_span_size(AZ_SPAN_EMPTY) && az_span_ptr(x) == az_span_ptr(AZ_SPAN_EMPTY)) + +static uint32_t getSasTokenExpiration(const char* sasToken) +{ + const char SE[] = { '&', 's', 'e', '=' }; + uint32_t se_as_unix_time = 0; + + int i, j; + for (i = 0, j = 0; sasToken[i] != '\0'; i++) + { + if (sasToken[i] == SE[j]) + { + j++; + if (j == sizeof(SE)) + { + // i is still at the '=' position. We must advance it by 1. + i++; + break; + } + } + else + { + j = 0; + } + } + + if (j != sizeof(SE)) + { + Logger.Error("Failed finding `se` field in SAS token"); + } + else + { + int k = i; + while (sasToken[k] != '\0' && sasToken[k] != '&') { k++; } + + if (az_result_failed( + az_span_atou32(az_span_create((uint8_t*)sasToken + i, k - i), &se_as_unix_time))) + { + Logger.Error("Failed parsing SAS token expiration timestamp"); + } + } + + return se_as_unix_time; +} + +static void mbedtls_hmac_sha256(az_span key, az_span payload, az_span signed_payload) +{ + mbedtls_md_context_t ctx; + mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256; + + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1); + mbedtls_md_hmac_starts(&ctx, (const unsigned char*)az_span_ptr(key), az_span_size(key)); + mbedtls_md_hmac_update(&ctx, (const unsigned char*)az_span_ptr(payload), az_span_size(payload)); + mbedtls_md_hmac_finish(&ctx, (byte*)az_span_ptr(signed_payload)); + mbedtls_md_free(&ctx); +} + +static void hmac_sha256_sign_signature( + az_span decoded_key, + az_span signature, + az_span signed_signature, + az_span* out_signed_signature) +{ + mbedtls_hmac_sha256(decoded_key, signature, signed_signature); + *out_signed_signature = az_span_slice(signed_signature, 0, 32); +} + +static void base64_encode_bytes( + az_span decoded_bytes, + az_span base64_encoded_bytes, + az_span* out_base64_encoded_bytes) +{ + size_t len; + if (mbedtls_base64_encode( + az_span_ptr(base64_encoded_bytes), + (size_t)az_span_size(base64_encoded_bytes), + &len, + az_span_ptr(decoded_bytes), + (size_t)az_span_size(decoded_bytes)) + != 0) + { + Logger.Error("mbedtls_base64_encode fail"); + } + + *out_base64_encoded_bytes = az_span_create(az_span_ptr(base64_encoded_bytes), (int32_t)len); +} + +static int decode_base64_bytes( + az_span base64_encoded_bytes, + az_span decoded_bytes, + az_span* out_decoded_bytes) +{ + memset(az_span_ptr(decoded_bytes), 0, (size_t)az_span_size(decoded_bytes)); + + size_t len; + if (mbedtls_base64_decode( + az_span_ptr(decoded_bytes), + (size_t)az_span_size(decoded_bytes), + &len, + az_span_ptr(base64_encoded_bytes), + (size_t)az_span_size(base64_encoded_bytes)) + != 0) + { + Logger.Error("mbedtls_base64_decode fail"); + return 1; + } + else + { + *out_decoded_bytes = az_span_create(az_span_ptr(decoded_bytes), (int32_t)len); + return 0; + } +} + +static int iot_sample_generate_sas_base64_encoded_signed_signature( + az_span sas_base64_encoded_key, + az_span sas_signature, + az_span sas_base64_encoded_signed_signature, + az_span* out_sas_base64_encoded_signed_signature) +{ + // Decode the sas base64 encoded key to use for HMAC signing. + char sas_decoded_key_buffer[32]; + az_span sas_decoded_key = AZ_SPAN_FROM_BUFFER(sas_decoded_key_buffer); + + if (decode_base64_bytes(sas_base64_encoded_key, sas_decoded_key, &sas_decoded_key) != 0) + { + Logger.Error("Failed generating encoded signed signature"); + return 1; + } + + // HMAC-SHA256 sign the signature with the decoded key. + char sas_hmac256_signed_signature_buffer[32]; + az_span sas_hmac256_signed_signature = AZ_SPAN_FROM_BUFFER(sas_hmac256_signed_signature_buffer); + hmac_sha256_sign_signature( + sas_decoded_key, sas_signature, sas_hmac256_signed_signature, &sas_hmac256_signed_signature); + + // Base64 encode the result of the HMAC signing. + base64_encode_bytes( + sas_hmac256_signed_signature, + sas_base64_encoded_signed_signature, + out_sas_base64_encoded_signed_signature); + + return 0; +} + +int64_t iot_sample_get_epoch_expiration_time_from_minutes(uint32_t minutes) +{ + time_t now = time(NULL); + return (int64_t)(now + minutes * 60); +} + +az_span generate_sas_token( + az_iot_hub_client* hub_client, + az_span device_key, + az_span sas_signature, + unsigned int expiryTimeInMinutes, + az_span sas_token) +{ + az_result rc; + // Create the POSIX expiration time from input minutes. + uint64_t sas_duration = iot_sample_get_epoch_expiration_time_from_minutes(expiryTimeInMinutes); + + // Get the signature that will later be signed with the decoded key. + // az_span sas_signature = AZ_SPAN_FROM_BUFFER(signature); + rc = az_iot_hub_client_sas_get_signature(hub_client, sas_duration, sas_signature, &sas_signature); + if (az_result_failed(rc)) + { + Logger.Error("Could not get the signature for SAS key: az_result return code " + rc); + return AZ_SPAN_EMPTY; + } + + // Generate the encoded, signed signature (b64 encoded, HMAC-SHA256 signing). + char b64enc_hmacsha256_signature[64]; + az_span sas_base64_encoded_signed_signature = AZ_SPAN_FROM_BUFFER(b64enc_hmacsha256_signature); + + if (iot_sample_generate_sas_base64_encoded_signed_signature( + device_key, + sas_signature, + sas_base64_encoded_signed_signature, + &sas_base64_encoded_signed_signature) != 0) + { + Logger.Error("Failed generating SAS token signed signature"); + return AZ_SPAN_EMPTY; + } + + // Get the resulting MQTT password, passing the base64 encoded, HMAC signed bytes. + size_t mqtt_password_length; + rc = az_iot_hub_client_sas_get_password( + hub_client, + sas_duration, + sas_base64_encoded_signed_signature, + AZ_SPAN_EMPTY, + (char*)az_span_ptr(sas_token), + az_span_size(sas_token), + &mqtt_password_length); + + if (az_result_failed(rc)) + { + Logger.Error("Could not get the password: az_result return code " + rc); + return AZ_SPAN_EMPTY; + } + else + { + return az_span_slice(sas_token, 0, mqtt_password_length); + } +} + +AzIoTSasToken::AzIoTSasToken( + az_iot_hub_client* client, + az_span deviceKey, + az_span signatureBuffer, + az_span sasTokenBuffer) +{ + this->client = client; + this->deviceKey = deviceKey; + this->signatureBuffer = signatureBuffer; + this->sasTokenBuffer = sasTokenBuffer; + this->expirationUnixTime = 0; + this->sasToken = AZ_SPAN_EMPTY; +} + +int AzIoTSasToken::Generate(unsigned int expiryTimeInMinutes) +{ + this->sasToken = generate_sas_token( + this->client, + this->deviceKey, + this->signatureBuffer, + expiryTimeInMinutes, + this->sasTokenBuffer); + + if (az_span_is_empty(this->sasToken)) + { + Logger.Error("Failed generating SAS token"); + return 1; + } + else + { + this->expirationUnixTime = getSasTokenExpiration((const char*)az_span_ptr(this->sasToken)); + + if (this->expirationUnixTime == 0) + { + Logger.Error("Failed getting the SAS token expiration time"); + this->sasToken = AZ_SPAN_EMPTY; + return 1; + } + else + { + return 0; + } + } +} + +bool AzIoTSasToken::IsExpired() +{ + time_t now = time(NULL); + + if (now == INDEFINITE_TIME) + { + Logger.Error("Failed getting current time"); + return true; + } + else + { + return (now >= this->expirationUnixTime); + } +} + +az_span AzIoTSasToken::Get() { return this->sasToken; } diff --git a/examples/Azure_IoT_Hub_ESP32/AzIoTSasToken.h b/examples/Azure_IoT_Hub_ESP32/AzIoTSasToken.h new file mode 100644 index 00000000..60ef9644 --- /dev/null +++ b/examples/Azure_IoT_Hub_ESP32/AzIoTSasToken.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#ifndef AZIOTSASTOKEN_H +#define AZIOTSASTOKEN_H + +#include +#include +#include + +class AzIoTSasToken +{ +public: + AzIoTSasToken( + az_iot_hub_client* client, + az_span deviceKey, + az_span signatureBuffer, + az_span sasTokenBuffer); + int Generate(unsigned int expiryTimeInMinutes); + bool IsExpired(); + az_span Get(); + +private: + az_iot_hub_client* client; + az_span deviceKey; + az_span signatureBuffer; + az_span sasTokenBuffer; + az_span sasToken; + uint32_t expirationUnixTime; +}; + +#endif // AZIOTSASTOKEN_H diff --git a/examples/Azure_IoT_Hub_ESP32/Azure_IoT_Hub_ESP32.ino b/examples/Azure_IoT_Hub_ESP32/Azure_IoT_Hub_ESP32.ino new file mode 100644 index 00000000..fd47ba98 --- /dev/null +++ b/examples/Azure_IoT_Hub_ESP32/Azure_IoT_Hub_ESP32.ino @@ -0,0 +1,368 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/* + * This is an Arduino-based Azure IoT Hub sample for ESPRESSIF ESP32 boards. + * It uses our Azure Embedded SDK for C to help interact with Azure IoT. + * For reference, please visit https://github.com/azure/azure-sdk-for-c. + * + * To connect and work with Azure IoT Hub you need a MQTT client, connecting, subscribing + * and publishing to specific topics to use the messaging features of the hub. + * Our azure-sdk-for-c is a MQTT client support library, helping composing and parsing the + * MQTT topic names and messages exchanged with the Azure IoT Hub. + * + * This sample performs the following tasks: + * - Synchronize the device clock with a NTP server; + * - Initialize our "az_iot_hub_client" (struct for data, part of our azure-sdk-for-c); + * - Initialize the MQTT client (here we use ESPRESSIF's esp_mqtt_client, which also handle the tcp connection and TLS); + * - Connect the MQTT client (using server-certificate validation, SAS-tokens for client authentication); + * - Periodically send telemetry data to the Azure IoT Hub. + * + * To properly connect to your Azure IoT Hub, please fill the information in the `iot_configs.h` file. + */ + +// C99 libraries +#include +#include +#include + +// Libraries for MQTT client and WiFi connection +#include +#include + +// Azure IoT SDK for C includes +#include +#include +#include + +// Additional sample headers +#include "AzIoTSasToken.h" +#include "SerialLogger.h" +#include "iot_configs.h" + +// When developing for your own Arduino-based platform, +// please follow the format '(ard;)'. +#define AZURE_SDK_CLIENT_USER_AGENT "c/" AZ_SDK_VERSION_STRING "(ard;esp32)" + +// Utility macros and defines +#define sizeofarray(a) (sizeof(a) / sizeof(a[0])) +#define NTP_SERVERS "pool.ntp.org", "time.nist.gov" +#define MQTT_QOS1 1 +#define DO_NOT_RETAIN_MSG 0 +#define SAS_TOKEN_DURATION_IN_MINUTES 60 +#define UNIX_TIME_NOV_13_2017 1510592825 + +#define PST_TIME_ZONE -8 +#define PST_TIME_ZONE_DAYLIGHT_SAVINGS_DIFF 1 + +#define GMT_OFFSET_SECS (PST_TIME_ZONE * 3600) +#define GMT_OFFSET_SECS_DST ((PST_TIME_ZONE + PST_TIME_ZONE_DAYLIGHT_SAVINGS_DIFF) * 3600) + +// Translate iot_configs.h defines into variables used by the sample +static const char* ssid = IOT_CONFIG_WIFI_SSID; +static const char* password = IOT_CONFIG_WIFI_PASSWORD; +static const char* host = IOT_CONFIG_IOTHUB_FQDN; +static const char* mqtt_broker_uri = "mqtts://" IOT_CONFIG_IOTHUB_FQDN; +static const char* device_id = IOT_CONFIG_DEVICE_ID; +static const int mqtt_port = 8883; + +// Memory allocated for the sample's variables and structures. +static esp_mqtt_client_handle_t mqtt_client; +static az_iot_hub_client client; + +static char mqtt_client_id[128]; +static char mqtt_username[128]; +static char mqtt_password[200]; +static uint8_t sas_signature_buffer[256]; +static unsigned long next_telemetry_send_time_ms = 0; +static char telemetry_topic[128]; +static uint8_t telemetry_payload[100]; +static uint32_t telemetry_send_count = 0; + +#define INCOMING_DATA_BUFFER_SIZE 128 +static char incoming_data[INCOMING_DATA_BUFFER_SIZE]; + +// Auxiliary functions + +static AzIoTSasToken sasToken( + &client, + AZ_SPAN_FROM_STR(IOT_CONFIG_DEVICE_KEY), + AZ_SPAN_FROM_BUFFER(sas_signature_buffer), + AZ_SPAN_FROM_BUFFER(mqtt_password)); + +static void connectToWiFi() +{ + Logger.Info("Connecting to WIFI SSID " + String(ssid)); + + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) + { + delay(500); + Serial.print("."); + } + + Serial.println(""); + + Logger.Info("WiFi connected, IP address: " + WiFi.localIP().toString()); +} + +static void initializeTime() +{ + Logger.Info("Setting time using SNTP"); + + configTime(GMT_OFFSET_SECS, GMT_OFFSET_SECS_DST, NTP_SERVERS); + time_t now = time(NULL); + while (now < UNIX_TIME_NOV_13_2017) + { + delay(500); + Serial.print("."); + now = time(nullptr); + } + Serial.println(""); + Logger.Info("Time initialized!"); +} + +void receivedCallback(char* topic, byte* payload, unsigned int length) +{ + Logger.Info("Received ["); + Logger.Info(topic); + Logger.Info("]: "); + for (int i = 0; i < length; i++) + { + Serial.print((char)payload[i]); + } +} + +static esp_err_t mqtt_event_handler(esp_mqtt_event_handle_t event) +{ + switch (event->event_id) + { + int i, r; + + case MQTT_EVENT_ERROR: + Logger.Info("MQTT event MQTT_EVENT_ERROR"); + break; + case MQTT_EVENT_CONNECTED: + Logger.Info("MQTT event MQTT_EVENT_CONNECTED"); + + r = esp_mqtt_client_subscribe(mqtt_client, AZ_IOT_HUB_CLIENT_C2D_SUBSCRIBE_TOPIC, 1); + if (r == -1) + { + Logger.Error("Could not subscribe for cloud-to-device messages."); + } + else + { + Logger.Info("Subscribed for cloud-to-device messages; message id:" + String(r)); + } + + break; + case MQTT_EVENT_DISCONNECTED: + Logger.Info("MQTT event MQTT_EVENT_DISCONNECTED"); + break; + case MQTT_EVENT_SUBSCRIBED: + Logger.Info("MQTT event MQTT_EVENT_SUBSCRIBED"); + break; + case MQTT_EVENT_UNSUBSCRIBED: + Logger.Info("MQTT event MQTT_EVENT_UNSUBSCRIBED"); + break; + case MQTT_EVENT_PUBLISHED: + Logger.Info("MQTT event MQTT_EVENT_PUBLISHED"); + break; + case MQTT_EVENT_DATA: + Logger.Info("MQTT event MQTT_EVENT_DATA"); + + for (i = 0; i < (INCOMING_DATA_BUFFER_SIZE - 1) && i < event->topic_len; i++) + { + incoming_data[i] = event->topic[i]; + } + incoming_data[i] = '\0'; + Logger.Info("Topic: " + String(incoming_data)); + + for (i = 0; i < (INCOMING_DATA_BUFFER_SIZE - 1) && i < event->data_len; i++) + { + incoming_data[i] = event->data[i]; + } + incoming_data[i] = '\0'; + Logger.Info("Data: " + String(incoming_data)); + + break; + case MQTT_EVENT_BEFORE_CONNECT: + Logger.Info("MQTT event MQTT_EVENT_BEFORE_CONNECT"); + break; + default: + Logger.Error("MQTT event UNKNOWN"); + break; + } + + return ESP_OK; +} + +static void initializeIoTHubClient() +{ + az_iot_hub_client_options options = az_iot_hub_client_options_default(); + options.user_agent = AZ_SPAN_FROM_STR(AZURE_SDK_CLIENT_USER_AGENT); + + if (az_result_failed(az_iot_hub_client_init( + &client, + az_span_create((uint8_t*)host, strlen(host)), + az_span_create((uint8_t*)device_id, strlen(device_id)), + &options))) + { + Logger.Error("Failed initializing Azure IoT Hub client"); + return; + } + + size_t client_id_length; + if (az_result_failed(az_iot_hub_client_get_client_id( + &client, mqtt_client_id, sizeof(mqtt_client_id) - 1, &client_id_length))) + { + Logger.Error("Failed getting client id"); + return; + } + + if (az_result_failed(az_iot_hub_client_get_user_name( + &client, mqtt_username, sizeofarray(mqtt_username), NULL))) + { + Logger.Error("Failed to get MQTT clientId, return code"); + return; + } + + Logger.Info("Client ID: " + String(mqtt_client_id)); + Logger.Info("Username: " + String(mqtt_username)); +} + +static int initializeMqttClient() +{ + if (sasToken.Generate(SAS_TOKEN_DURATION_IN_MINUTES) != 0) + { + Logger.Error("Failed generating SAS token"); + return 1; + } + + esp_mqtt_client_config_t mqtt_config; + memset(&mqtt_config, 0, sizeof(mqtt_config)); + mqtt_config.uri = mqtt_broker_uri; + mqtt_config.port = mqtt_port; + mqtt_config.client_id = mqtt_client_id; + mqtt_config.username = mqtt_username; + mqtt_config.password = (const char*)az_span_ptr(sasToken.Get()); + mqtt_config.keepalive = 30; + mqtt_config.disable_clean_session = 0; + mqtt_config.disable_auto_reconnect = false; + mqtt_config.event_handle = mqtt_event_handler; + mqtt_config.user_context = NULL; + mqtt_config.cert_pem = (const char*)ca_pem; + + mqtt_client = esp_mqtt_client_init(&mqtt_config); + + if (mqtt_client == NULL) + { + Logger.Error("Failed creating mqtt client"); + return 1; + } + + esp_err_t start_result = esp_mqtt_client_start(mqtt_client); + + if (start_result != ESP_OK) + { + Logger.Error("Could not start mqtt client; error code:" + start_result); + return 1; + } + else + { + Logger.Info("MQTT client started"); + return 0; + } +} + +/* + * @brief Gets the number of seconds since UNIX epoch until now. + * @return uint32_t Number of seconds. + */ +static uint32_t getEpochTimeInSecs() +{ + return (uint32_t)time(NULL); +} + +static void establishConnection() +{ + connectToWiFi(); + initializeTime(); + initializeIoTHubClient(); + (void)initializeMqttClient(); +} + +static void getTelemetryPayload(az_span payload, az_span* out_payload) +{ + az_span original_payload = payload; + + payload = az_span_copy( + payload, AZ_SPAN_FROM_STR("{ \"msgCount\": ")); + (void)az_span_u32toa(payload, telemetry_send_count++, &payload); + payload = az_span_copy(payload, AZ_SPAN_FROM_STR(" }")); + payload = az_span_copy_u8(payload, '\0'); + + *out_payload = az_span_slice(original_payload, 0, az_span_size(original_payload) - az_span_size(payload)); +} + +static void sendTelemetry() +{ + az_span telemetry = AZ_SPAN_FROM_BUFFER(telemetry_payload); + + Logger.Info("Sending telemetry ..."); + + // The topic could be obtained just once during setup, + // however if properties are used the topic need to be generated again to reflect the + // current values of the properties. + if (az_result_failed(az_iot_hub_client_telemetry_get_publish_topic( + &client, NULL, telemetry_topic, sizeof(telemetry_topic), NULL))) + { + Logger.Error("Failed az_iot_hub_client_telemetry_get_publish_topic"); + return; + } + + getTelemetryPayload(telemetry, &telemetry); + + if (esp_mqtt_client_publish( + mqtt_client, + telemetry_topic, + (const char*)az_span_ptr(telemetry), + az_span_size(telemetry), + MQTT_QOS1, + DO_NOT_RETAIN_MSG) + == 0) + { + Logger.Error("Failed publishing"); + } + else + { + Logger.Info("Message published successfully"); + } +} + +// Arduino setup and loop main functions. + +void setup() +{ + establishConnection(); +} + +void loop() +{ + if (WiFi.status() != WL_CONNECTED) + { + connectToWiFi(); + } + else if (sasToken.IsExpired()) + { + Logger.Info("SAS token expired; reconnecting with a new one."); + (void)esp_mqtt_client_destroy(mqtt_client); + initializeMqttClient(); + } + else if (millis() > next_telemetry_send_time_ms) + { + sendTelemetry(); + next_telemetry_send_time_ms = millis() + TELEMETRY_FREQUENCY_MILLISECS; + } +} diff --git a/examples/Azure_IoT_Hub_ESP32/SerialLogger.cpp b/examples/Azure_IoT_Hub_ESP32/SerialLogger.cpp new file mode 100644 index 00000000..1e16dd04 --- /dev/null +++ b/examples/Azure_IoT_Hub_ESP32/SerialLogger.cpp @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "SerialLogger.h" +#include + +#define UNIX_EPOCH_START_YEAR 1900 + +static void writeTime() +{ + struct tm* ptm; + time_t now = time(NULL); + + ptm = gmtime(&now); + + Serial.print(ptm->tm_year + UNIX_EPOCH_START_YEAR); + Serial.print("/"); + Serial.print(ptm->tm_mon + 1); + Serial.print("/"); + Serial.print(ptm->tm_mday); + Serial.print(" "); + + if (ptm->tm_hour < 10) + { + Serial.print(0); + } + + Serial.print(ptm->tm_hour); + Serial.print(":"); + + if (ptm->tm_min < 10) + { + Serial.print(0); + } + + Serial.print(ptm->tm_min); + Serial.print(":"); + + if (ptm->tm_sec < 10) + { + Serial.print(0); + } + + Serial.print(ptm->tm_sec); +} + +SerialLogger::SerialLogger() { Serial.begin(SERIAL_LOGGER_BAUD_RATE); } + +void SerialLogger::Info(String message) +{ + writeTime(); + Serial.print(" [INFO] "); + Serial.println(message); +} + +void SerialLogger::Error(String message) +{ + writeTime(); + Serial.print(" [ERROR] "); + Serial.println(message); +} + +SerialLogger Logger; diff --git a/examples/Azure_IoT_Hub_ESP32/SerialLogger.h b/examples/Azure_IoT_Hub_ESP32/SerialLogger.h new file mode 100644 index 00000000..66f918b6 --- /dev/null +++ b/examples/Azure_IoT_Hub_ESP32/SerialLogger.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#ifndef SERIALLOGGER_H +#define SERIALLOGGER_H + +#include + +#ifndef SERIAL_LOGGER_BAUD_RATE +#define SERIAL_LOGGER_BAUD_RATE 115200 +#endif + +class SerialLogger +{ +public: + SerialLogger(); + void Info(String message); + void Error(String message); +}; + +extern SerialLogger Logger; + +#endif // SERIALLOGGER_H diff --git a/examples/Azure_IoT_Hub_ESP32/iot_configs.h b/examples/Azure_IoT_Hub_ESP32/iot_configs.h new file mode 100644 index 00000000..9a05c1cd --- /dev/null +++ b/examples/Azure_IoT_Hub_ESP32/iot_configs.h @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +// Wifi +#define IOT_CONFIG_WIFI_SSID "SSID" +#define IOT_CONFIG_WIFI_PASSWORD "PWD" + +// Azure IoT +#define IOT_CONFIG_IOTHUB_FQDN "[your Azure IoT host name].azure-devices.net" +#define IOT_CONFIG_DEVICE_ID "Device ID" +#define IOT_CONFIG_DEVICE_KEY "Device Key" + +// Publish 1 message every 2 seconds +#define TELEMETRY_FREQUENCY_MILLISECS 2000 diff --git a/examples/Azure_IoT_Hub_ESP32/readme.md b/examples/Azure_IoT_Hub_ESP32/readme.md new file mode 100644 index 00000000..5a3f26f1 --- /dev/null +++ b/examples/Azure_IoT_Hub_ESP32/readme.md @@ -0,0 +1,255 @@ +--- +page_type: sample +description: Connecting an ESP32 device to Azure IoT using the Azure SDK for Embedded C +languages: +- c +products: +- azure-iot +- azure-iot-pnp +- azure-iot-dps +- azure-iot-hub +--- + +# How to Setup and Run Azure SDK for Embedded C IoT Hub Client on Espressif ESP32 + + - [How to Setup and Run Azure SDK for Embedded C IoT Hub Client on Espressif ESP32](#how-to-setup-and-run-azure-sdk-for-embedded-c-iot-hub-client-on-espressif-esp32) + - [Introduction](#introduction) + - [What is Covered](#what-is-covered) + - [Prerequisites](#prerequisites) + - [Setup and Run Instructions](#setup-and-run-instructions) + - [Troubleshooting](#troubleshooting) + - [Contributing](#contributing) + - [License](#license) + +## Introduction + +This is a "to-the-point" guide outlining how to run an Azure SDK for Embedded C IoT Hub telemetry sample on an ESP32 microcontroller. The command line examples are tailored to Debian/Ubuntu environments. + +### What is Covered + +- Configuration instructions for the Arduino IDE to compile a sample using the [Azure SDK for Embedded C](https://github.com/Azure/azure-sdk-for-c). +- Configuration, build, and run instructions for the IoT Hub telemetry sample. + +_The following was run on Windows 10 and Ubuntu Desktop 20.04 environments, with Arduino IDE 1.8.15 and ESP32 board library version 1.0.6._ + +## Prerequisites + +- Have an [Azure account](https://azure.microsoft.com/) created. +- Have an [Azure IoT Hub](https://docs.microsoft.com/azure/iot-hub/iot-hub-create-through-portal) created. +- Have a [logical device](https://docs.microsoft.com/azure/iot-hub/iot-hub-create-through-portal#register-a-new-device-in-the-iot-hub) created in your Azure IoT Hub using the authentication type "Symmetric Key". + + NOTE: Device keys are used to automatically generate a SAS token for authentication. + +- Have the latest [Arduino IDE](https://www.arduino.cc/en/Main/Software) installed. + +- Have the [ESP32 board support](https://github.com/espressif/arduino-esp32) installed on Arduino IDE. + + - ESP32 boards are not natively supported by Arduino IDE, so you need to add them manually. + - Follow the [instructions](https://github.com/espressif/arduino-esp32) in the official ESP32 repository. + +- Have one of the following interfaces to your Azure IoT Hub set up: + - [Azure Command Line Interface](https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest) (Azure CLI) utility installed, along with the [Azure IoT CLI extension](https://github.com/Azure/azure-iot-cli-extension). + + On Windows: + + Download and install: https://aka.ms/installazurecliwindows + + ```powershell + PS C:\>az extension add --name azure-iot + ``` + + On Linux: + + ```bash + $ curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + $ az extension add --name azure-iot + ``` + + A list of all the Azure IoT CLI extension commands can be found [here](https://docs.microsoft.com/cli/azure/iot?view=azure-cli-latest). + + - The most recent version of [Azure IoT Explorer](https://github.com/Azure/azure-iot-explorer/releases) installed. More instruction on its usage can be found [here](https://docs.microsoft.com/azure/iot-pnp/howto-use-iot-explorer). + + NOTE: This guide demonstrates use of the Azure CLI and does NOT demonstrate use of Azure IoT Explorer. + +## Setup and Run Instructions + +1. Run the Arduino IDE. + +2. Install the Azure SDK for Embedded C library. + + - On the Arduino IDE, go to menu `Sketch`, `Include Library`, `Manage Libraries...`. + - Search for and install `azure-sdk-for-c`. + +3. Open the ESPRESSIF ESP32 sample. + + - On the Arduino IDE, go to menu `File`, `Examples`, `azure-sdk-for-c`. + - Click on `az_esp32` to open the sample. + +4. Configure the ESPRESSIF ESP32 sample. + + Enter your Azure IoT Hub and device information into the sample's `iot_configs.h`. + +5. Connect the ESP32 microcontroller to your USB port. + +6. On the Arduino IDE, select the board and port. + + - Go to menu `Tools`, `Board` and select `ESP32`. + - Go to menu `Tools`, `Port` and select the port to which the microcontroller is connected. + +7. Upload the sketch. + + - Go to menu `Sketch` and click on `Upload`. + +
Expected output of the upload: +

+ + ```text + Executable segment sizes: + IROM : 361788 - code in flash (default or ICACHE_FLASH_ATTR) + IRAM : 26972 / 32768 - code in IRAM (ICACHE_RAM_ATTR, ISRs...) + DATA : 1360 ) - initialized variables (global, static) in RAM/HEAP + RODATA : 2152 ) / 81920 - constants (global, static) in RAM/HEAP + BSS : 26528 ) - zeroed variables (global, static) in RAM/HEAP + Sketch uses 392272 bytes (37%) of program storage space. Maximum is 1044464 bytes. + Global variables use 30040 bytes (36%) of dynamic memory, leaving 51880 bytes for local variables. Maximum is 81920 bytes. + /home/user/.arduino15/packages/esp8266/tools/python3/3.7.2-post1/python3 /home/user/.arduino15/packages/esp8266/hardware/esp8266/2.7.1/tools/upload.py --chip esp8266 --port /dev/ttyUSB0 --baud 230400 --before default_reset --after hard_reset write_flash 0x0 /tmp/arduino_build_826987/azure_iot_hub_telemetry.ino.bin + esptool.py v2.8 + Serial port /dev/ttyUSB0 + Connecting.... + Chip is ESP8266EX + Features: WiFi + Crystal is 26MHz + MAC: dc:4f:22:5e:a7:09 + Uploading stub... + Running stub... + Stub running... + Changing baud rate to 230400 + Changed. + Configuring flash size... + Auto-detected Flash size: 4MB + Compressed 396432 bytes to 292339... + + Writing at 0x00000000... (5 %) + Writing at 0x00004000... (11 %) + Writing at 0x00008000... (16 %) + Writing at 0x0000c000... (22 %) + Writing at 0x00010000... (27 %) + Writing at 0x00014000... (33 %) + Writing at 0x00018000... (38 %) + Writing at 0x0001c000... (44 %) + Writing at 0x00020000... (50 %) + Writing at 0x00024000... (55 %) + Writing at 0x00028000... (61 %) + Writing at 0x0002c000... (66 %) + Writing at 0x00030000... (72 %) + Writing at 0x00034000... (77 %) + Writing at 0x00038000... (83 %) + Writing at 0x0003c000... (88 %) + Writing at 0x00040000... (94 %) + Writing at 0x00044000... (100 %) + Wrote 396432 bytes (292339 compressed) at 0x00000000 in 13.0 seconds (effective 243.4 kbit/s)... + Hash of data verified. + + Leaving... + Hard resetting via RTS pin... + ``` + +

+
+ +8. Monitor the MCU (microcontroller) locally via the Serial Port. + + - Go to menu `Tools`, `Serial Monitor`. + + If you perform this step right away after uploading the sketch, the serial monitor will show an output similar to the following upon success: + + ```text + Connecting to WIFI SSID buckaroo + .......................WiFi connected, IP address: + 192.168.1.123 + Setting time using SNTP..............................done! + Current time: Thu May 28 02:55:05 2020 + Client ID: mydeviceid + Username: myiothub.azure-devices.net/mydeviceid/?api-version=2018-06-30&DeviceClientType=c%2F1.0.0 + SharedAccessSignature sr=myiothub.azure-devices.net%2Fdevices%2Fmydeviceid&sig=placeholder-password&se=1590620105 + MQTT connecting ... connected. + ``` + +9. Monitor the telemetry messages sent to the Azure IoT Hub using the connection string for the policy name `iothubowner` found under "Shared access policies" on your IoT Hub in the Azure portal. + + ```bash + $ az iot hub monitor-events --login --device-id + ``` + +
Expected telemetry output: +

+ + ```bash + Starting event monitor, filtering on device: mydeviceid, use ctrl-c to stop... + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + ^CStopping event monitor... + ``` + +

+
+ +## Certificates - Important to know + +The Azure IoT service certificates presented during TLS negotiation shall be always validated, on the device, using the appropriate trusted root CA certificate(s). + +For the ESP32 sample, our script `generate_arduino_zip_library.sh` automatically downloads the root certificate used in the United States regions (Baltimore CA certificate) and adds it to the Arduino sketch project. + +For other regions (and private cloud environments), please use the appropriate root CA certificate. + +### Additional Information + +For important information and additional guidance about certificates, please refer to [this blog post](https://techcommunity.microsoft.com/t5/internet-of-things/azure-iot-tls-changes-are-coming-and-why-you-should-care/ba-p/1658456) from the security team. + +## Troubleshooting + +- The error policy for the Embedded C SDK client library is documented [here](https://github.com/Azure/azure-sdk-for-c/blob/main/sdk/docs/iot/mqtt_state_machine.md#error-policy). +- File an issue via [Github Issues](https://github.com/Azure/azure-sdk-for-c/issues/new/choose). +- Check [previous questions](https://stackoverflow.com/questions/tagged/azure+c) or ask new ones on StackOverflow using the `azure` and `c` tags. + +## Contributing + +This project welcomes contributions and suggestions. Find more contributing details [here](https://github.com/Azure/azure-sdk-for-c/blob/main/CONTRIBUTING.md). + +### License + +Azure SDK for Embedded C is licensed under the [MIT](https://github.com/Azure/azure-sdk-for-c/blob/main/LICENSE) license. diff --git a/examples/Azure_IoT_Hub_ESP8266/Azure_IoT_Hub_ESP8266.ino b/examples/Azure_IoT_Hub_ESP8266/Azure_IoT_Hub_ESP8266.ino new file mode 100644 index 00000000..72ca043b --- /dev/null +++ b/examples/Azure_IoT_Hub_ESP8266/Azure_IoT_Hub_ESP8266.ino @@ -0,0 +1,362 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/* + * This is an Arduino-based Azure IoT Hub sample for ESPRESSIF ESP8266 board. + * It uses our Azure Embedded SDK for C to help interact with Azure IoT. + * For reference, please visit https://github.com/azure/azure-sdk-for-c. + * + * To connect and work with Azure IoT Hub you need a MQTT client, connecting, subscribing + * and publishing to specific topics to use the messaging features of the hub. + * Our azure-sdk-for-c is an MQTT client support library, helping to compose and parse the + * MQTT topic names and messages exchanged with the Azure IoT Hub. + * + * This sample performs the following tasks: + * - Synchronize the device clock with a NTP server; + * - Initialize our "az_iot_hub_client" (struct for data, part of our azure-sdk-for-c); + * - Initialize the MQTT client (here we use Nick Oleary's PubSubClient, which also handle the tcp connection and TLS); + * - Connect the MQTT client (using server-certificate validation, SAS-tokens for client authentication); + * - Periodically send telemetry data to the Azure IoT Hub. + * + * To properly connect to your Azure IoT Hub, please fill the information in the `iot_configs.h` file. + */ + +// C99 libraries +#include +#include +#include +#include + +// Libraries for MQTT client, WiFi connection and SAS-token generation. +#include +#include +#include +#include +#include +#include +#include + +// Azure IoT SDK for C includes +#include +#include +#include + +// Additional sample headers +#include "iot_configs.h" + +// When developing for your own Arduino-based platform, +// please follow the format '(ard;)'. +#define AZURE_SDK_CLIENT_USER_AGENT "c/" AZ_SDK_VERSION_STRING "(ard;esp8266)" + +// Utility macros and defines +#define LED_PIN 2 +#define sizeofarray(a) (sizeof(a) / sizeof(a[0])) +#define ONE_HOUR_IN_SECS 3600 +#define NTP_SERVERS "pool.ntp.org", "time.nist.gov" +#define MQTT_PACKET_SIZE 1024 + +// Translate iot_configs.h defines into variables used by the sample +static const char* ssid = IOT_CONFIG_WIFI_SSID; +static const char* password = IOT_CONFIG_WIFI_PASSWORD; +static const char* host = IOT_CONFIG_IOTHUB_FQDN; +static const char* device_id = IOT_CONFIG_DEVICE_ID; +static const char* device_key = IOT_CONFIG_DEVICE_KEY; +static const int port = 8883; + +// Memory allocated for the sample's variables and structures. +static WiFiClientSecure wifi_client; +static X509List cert((const char*)ca_pem); +static PubSubClient mqtt_client(wifi_client); +static az_iot_hub_client client; +static char sas_token[200]; +static uint8_t signature[512]; +static unsigned char encrypted_signature[32]; +static char base64_decoded_device_key[32]; +static unsigned long next_telemetry_send_time_ms = 0; +static char telemetry_topic[128]; +static uint8_t telemetry_payload[100]; +static uint32_t telemetry_send_count = 0; + + +// Auxiliary functions + +static void connectToWiFi() +{ + Serial.begin(115200); + Serial.println(); + Serial.print("Connecting to WIFI SSID "); + Serial.println(ssid); + + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + while (WiFi.status() != WL_CONNECTED) + { + delay(500); + Serial.print("."); + } + + Serial.print("WiFi connected, IP address: "); + Serial.println(WiFi.localIP()); +} + +static void initializeTime() +{ + Serial.print("Setting time using SNTP"); + + configTime(-5 * 3600, 0, NTP_SERVERS); + time_t now = time(NULL); + while (now < 1510592825) + { + delay(500); + Serial.print("."); + now = time(NULL); + } + Serial.println("done!"); +} + +static char* getCurrentLocalTimeString() +{ + time_t now = time(NULL); + return ctime(&now); +} + +static void printCurrentTime() +{ + Serial.print("Current time: "); + Serial.print(getCurrentLocalTimeString()); +} + +void receivedCallback(char* topic, byte* payload, unsigned int length) +{ + Serial.print("Received ["); + Serial.print(topic); + Serial.print("]: "); + for (int i = 0; i < length; i++) + { + Serial.print((char)payload[i]); + } +} + +static void initializeClients() +{ + az_iot_hub_client_options options = az_iot_hub_client_options_default(); + options.user_agent = AZ_SPAN_FROM_STR(AZURE_SDK_CLIENT_USER_AGENT); + + wifi_client.setTrustAnchors(&cert); + if (az_result_failed(az_iot_hub_client_init( + &client, + az_span_create((uint8_t*)host, strlen(host)), + az_span_create((uint8_t*)device_id, strlen(device_id)), + &options))) + { + Serial.println("Failed initializing Azure IoT Hub client"); + return; + } + + mqtt_client.setServer(host, port); + mqtt_client.setCallback(receivedCallback); +} + +/* + * @brief Gets the number of seconds since UNIX epoch until now. + * @return uint32_t Number of seconds. + */ +static uint32_t getSecondsSinceEpoch() +{ + return (uint32_t)time(NULL); +} + +static int generateSasToken(char* sas_token, size_t size) +{ + az_span signature_span = az_span_create((uint8_t*)signature, sizeofarray(signature)); + az_span out_signature_span; + az_span encrypted_signature_span + = az_span_create((uint8_t*)encrypted_signature, sizeofarray(encrypted_signature)); + + uint32_t expiration = getSecondsSinceEpoch() + ONE_HOUR_IN_SECS; + + // Get signature + if (az_result_failed(az_iot_hub_client_sas_get_signature( + &client, expiration, signature_span, &out_signature_span))) + { + Serial.println("Failed getting SAS signature"); + return 1; + } + + // Base64-decode device key + int base64_decoded_device_key_length + = base64_decode_chars(device_key, strlen(device_key), base64_decoded_device_key); + + if (base64_decoded_device_key_length == 0) + { + Serial.println("Failed base64 decoding device key"); + return 1; + } + + // SHA-256 encrypt + br_hmac_key_context kc; + br_hmac_key_init( + &kc, &br_sha256_vtable, base64_decoded_device_key, base64_decoded_device_key_length); + + br_hmac_context hmac_ctx; + br_hmac_init(&hmac_ctx, &kc, 32); + br_hmac_update(&hmac_ctx, az_span_ptr(out_signature_span), az_span_size(out_signature_span)); + br_hmac_out(&hmac_ctx, encrypted_signature); + + // Base64 encode encrypted signature + String b64enc_hmacsha256_signature = base64::encode(encrypted_signature, br_hmac_size(&hmac_ctx)); + + az_span b64enc_hmacsha256_signature_span = az_span_create( + (uint8_t*)b64enc_hmacsha256_signature.c_str(), b64enc_hmacsha256_signature.length()); + + // URl-encode base64 encoded encrypted signature + if (az_result_failed(az_iot_hub_client_sas_get_password( + &client, + expiration, + b64enc_hmacsha256_signature_span, + AZ_SPAN_EMPTY, + sas_token, + size, + NULL))) + { + Serial.println("Failed getting SAS token"); + return 1; + } + + return 0; +} + +static int connectToAzureIoTHub() +{ + size_t client_id_length; + char mqtt_client_id[128]; + if (az_result_failed(az_iot_hub_client_get_client_id( + &client, mqtt_client_id, sizeof(mqtt_client_id) - 1, &client_id_length))) + { + Serial.println("Failed getting client id"); + return 1; + } + + mqtt_client_id[client_id_length] = '\0'; + + char mqtt_username[128]; + // Get the MQTT user name used to connect to IoT Hub + if (az_result_failed(az_iot_hub_client_get_user_name( + &client, mqtt_username, sizeofarray(mqtt_username), NULL))) + { + printf("Failed to get MQTT clientId, return code\n"); + return 1; + } + + Serial.print("Client ID: "); + Serial.println(mqtt_client_id); + + Serial.print("Username: "); + Serial.println(mqtt_username); + + mqtt_client.setBufferSize(MQTT_PACKET_SIZE); + + while (!mqtt_client.connected()) + { + time_t now = time(NULL); + + Serial.print("MQTT connecting ... "); + + if (mqtt_client.connect(mqtt_client_id, mqtt_username, sas_token)) + { + Serial.println("connected."); + } + else + { + Serial.print("failed, status code ="); + Serial.print(mqtt_client.state()); + Serial.println(". Trying again in 5 seconds."); + // Wait 5 seconds before retrying + delay(5000); + } + } + + mqtt_client.subscribe(AZ_IOT_HUB_CLIENT_C2D_SUBSCRIBE_TOPIC); + + return 0; +} + +static void establishConnection() +{ + connectToWiFi(); + initializeTime(); + printCurrentTime(); + initializeClients(); + + // The SAS token is valid for 1 hour by default in this sample. + // After one hour the sample must be restarted, or the client won't be able + // to connect/stay connected to the Azure IoT Hub. + if (generateSasToken(sas_token, sizeofarray(sas_token)) != 0) + { + Serial.println("Failed generating MQTT password"); + } + else + { + connectToAzureIoTHub(); + } + + digitalWrite(LED_PIN, LOW); +} + +static char* getTelemetryPayload() +{ + az_span temp_span = az_span_create(telemetry_payload, sizeof(telemetry_payload)); + temp_span = az_span_copy(temp_span, AZ_SPAN_FROM_STR("{ \"msgCount\": ")); + (void)az_span_u32toa(temp_span, telemetry_send_count++, &temp_span); + temp_span = az_span_copy(temp_span, AZ_SPAN_FROM_STR(" }")); + temp_span = az_span_copy_u8(temp_span, '\0'); + + return (char*)telemetry_payload; +} + +static void sendTelemetry() +{ + digitalWrite(LED_PIN, HIGH); + Serial.print(millis()); + Serial.print(" ESP8266 Sending telemetry . . . "); + if (az_result_failed(az_iot_hub_client_telemetry_get_publish_topic( + &client, NULL, telemetry_topic, sizeof(telemetry_topic), NULL))) + { + Serial.println("Failed az_iot_hub_client_telemetry_get_publish_topic"); + return; + } + + mqtt_client.publish(telemetry_topic, getTelemetryPayload(), false); + Serial.println("OK"); + delay(100); + digitalWrite(LED_PIN, LOW); +} + + +// Arduino setup and loop main functions. + +void setup() +{ + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, HIGH); + establishConnection(); +} + +void loop() +{ + if (millis() > next_telemetry_send_time_ms) + { + // Check if connected, reconnect if needed. + if(!mqtt_client.connected()) + { + establishConnection(); + } + + sendTelemetry(); + next_telemetry_send_time_ms = millis() + TELEMETRY_FREQUENCY_MILLISECS; + } + + // MQTT loop must be called to process Device-to-Cloud and Cloud-to-Device. + mqtt_client.loop(); + delay(500); +} diff --git a/examples/Azure_IoT_Hub_ESP8266/iot_configs.h b/examples/Azure_IoT_Hub_ESP8266/iot_configs.h new file mode 100644 index 00000000..9a05c1cd --- /dev/null +++ b/examples/Azure_IoT_Hub_ESP8266/iot_configs.h @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +// Wifi +#define IOT_CONFIG_WIFI_SSID "SSID" +#define IOT_CONFIG_WIFI_PASSWORD "PWD" + +// Azure IoT +#define IOT_CONFIG_IOTHUB_FQDN "[your Azure IoT host name].azure-devices.net" +#define IOT_CONFIG_DEVICE_ID "Device ID" +#define IOT_CONFIG_DEVICE_KEY "Device Key" + +// Publish 1 message every 2 seconds +#define TELEMETRY_FREQUENCY_MILLISECS 2000 diff --git a/examples/Azure_IoT_Hub_ESP8266/readme.md b/examples/Azure_IoT_Hub_ESP8266/readme.md new file mode 100644 index 00000000..4fe67474 --- /dev/null +++ b/examples/Azure_IoT_Hub_ESP8266/readme.md @@ -0,0 +1,265 @@ +--- +page_type: sample +description: Connecting an ESP8266 device to Azure IoT using the Azure SDK for Embedded C +languages: +- c +products: +- azure-iot +- azure-iot-pnp +- azure-iot-dps +- azure-iot-hub +--- + +# How to Setup and Run Azure SDK for Embedded C IoT Hub Client on Espressif ESP8266 NodeMCU + +- [How to Setup and Run Azure SDK for Embedded C IoT Hub Client on Espressif ESP8266 NodeMCU](#how-to-setup-and-run-azure-sdk-for-embedded-c-iot-hub-client-on-espressif-esp8266-nodemcu) + - [Introduction](#introduction) + - [What is Covered](#what-is-covered) + - [Prerequisites](#prerequisites) + - [Setup and Run Instructions](#setup-and-run-instructions) + - [Certificates - Important to know](#certificates---important-to-know) + - [Additional Information](#additional-information) + - [Troubleshooting](#troubleshooting) + - [Contributing](#contributing) + - [License](#license) + +## Introduction + +This is a "to-the-point" guide outlining how to run an Azure SDK for Embedded C IoT Hub telemetry sample on an Esp8266 NodeMCU microcontroller. The command line examples are tailored to Debian/Ubuntu environments. + +### What is Covered + +- Configuration instructions for the Arduino IDE to compile a sample using the Azure SDK for Embedded C. +- Configuration, build, and run instructions for the IoT Hub telemetry sample. + +_The following was run on Windows 10 and Ubuntu Desktop 20.04 environments, with Arduino IDE 1.8.15 and Esp8266 module 3.0.1._ + +## Prerequisites + +- Have an [Azure account](https://azure.microsoft.com/) created. +- Have an [Azure IoT Hub](https://docs.microsoft.com/azure/iot-hub/iot-hub-create-through-portal) created. +- Have a [logical device](https://docs.microsoft.com/azure/iot-hub/iot-hub-create-through-portal#register-a-new-device-in-the-iot-hub) created in your Azure IoT Hub using the authentication type "Symmetric Key". + + NOTE: Device keys are used to automatically generate a SAS token for authentication, which is only valid for one hour. + +- Have the latest [Arduino IDE](https://www.arduino.cc/en/Main/Software) installed. + +- Have the [ESP8266 board support](https://github.com/esp8266/Arduino#installing-with-boards-manager) installed on Arduino IDE. ESP8266 boards are not natively supported by Arduino IDE, so you need to add them manually. + + - ESP8266 boards are not natively supported by Arduino IDE, so you need to add them manually. + - Follow the [instructions](https://github.com/esp8266/Arduino#installing-with-boards-manager) in the official Esp8266 repository. + +- Have one of the following interfaces to your Azure IoT Hub set up: + - [Azure Command Line Interface](https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest) (Azure CLI) utility installed, along with the [Azure IoT CLI extension](https://github.com/Azure/azure-iot-cli-extension). + + On Windows: + + Download and install: https://aka.ms/installazurecliwindows + + ```powershell + PS C:\>az extension add --name azure-iot + ``` + + On Linux: + + ```bash + $ curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + $ az extension add --name azure-iot + ``` + + A list of all the Azure IoT CLI extension commands can be found [here](https://docs.microsoft.com/cli/azure/iot?view=azure-cli-latest). + + - The most recent version of [Azure IoT Explorer](https://github.com/Azure/azure-iot-explorer/releases) installed. More instruction on its usage can be found [here](https://docs.microsoft.com/azure/iot-pnp/howto-use-iot-explorer). + + NOTE: This guide demonstrates use of the Azure CLI and does NOT demonstrate use of Azure IoT Explorer. + +## Setup and Run Instructions + +1. Run the Arduino IDE. + +2. Install the Azure SDK for Embedded C library. + + - On the Arduino IDE, go to menu `Sketch`, `Include Library`, `Manage Libraries...`. + - Search for and install `azure-sdk-for-c`. + +3. Install the Arduino PubSubClient library. (PubSubClient is a popular MQTT client for Arduino.) + + - On the Arduino IDE, go to menu `Sketch`, `Include Library`, `Manage Libraries...`. + - Search for `PubSubClient` (by Nick O'Leary). + - Hover over the library item on the result list, then click on "Install". + +4. Open the ESPRESSIF ESP 8266 sample. + + - On the Arduino IDE, go to menu `File`, `Examples`, `azure-sdk-for-c`. + - Click on `az_esp8266` to open the sample. + +5. Configure the ESPRESSIF ESP 8266 sample. + + Enter your Azure IoT Hub and device information into the sample's `iot_configs.h`. + +6. Connect the Esp8266 NodeMCU microcontroller to your USB port. + +7. On the Arduino IDE, select the board and port. + + - Go to menu `Tools`, `Board` and select `NodeMCU 1.0 (ESP-12E Module)`. + - Go to menu `Tools`, `Port` and select the port to which the microcontroller is connected. + +8. Upload the sketch. + + - Go to menu `Sketch` and click on `Upload`. + +
Expected output of the upload: +

+ + ```text + Executable segment sizes: + IROM : 361788 - code in flash (default or ICACHE_FLASH_ATTR) + IRAM : 26972 / 32768 - code in IRAM (ICACHE_RAM_ATTR, ISRs...) + DATA : 1360 ) - initialized variables (global, static) in RAM/HEAP + RODATA : 2152 ) / 81920 - constants (global, static) in RAM/HEAP + BSS : 26528 ) - zeroed variables (global, static) in RAM/HEAP + Sketch uses 392272 bytes (37%) of program storage space. Maximum is 1044464 bytes. + Global variables use 30040 bytes (36%) of dynamic memory, leaving 51880 bytes for local variables. Maximum is 81920 bytes. + /home/user/.arduino15/packages/esp8266/tools/python3/3.7.2-post1/python3 /home/user/.arduino15/packages/esp8266/hardware/esp8266/2.7.1/tools/upload.py --chip esp8266 --port /dev/ttyUSB0 --baud 230400 --before default_reset --after hard_reset write_flash 0x0 /tmp/arduino_build_826987/azure_iot_hub_telemetry.ino.bin + esptool.py v2.8 + Serial port /dev/ttyUSB0 + Connecting.... + Chip is ESP8266EX + Features: WiFi + Crystal is 26MHz + MAC: dc:4f:22:5e:a7:09 + Uploading stub... + Running stub... + Stub running... + Changing baud rate to 230400 + Changed. + Configuring flash size... + Auto-detected Flash size: 4MB + Compressed 396432 bytes to 292339... + + Writing at 0x00000000... (5 %) + Writing at 0x00004000... (11 %) + Writing at 0x00008000... (16 %) + Writing at 0x0000c000... (22 %) + Writing at 0x00010000... (27 %) + Writing at 0x00014000... (33 %) + Writing at 0x00018000... (38 %) + Writing at 0x0001c000... (44 %) + Writing at 0x00020000... (50 %) + Writing at 0x00024000... (55 %) + Writing at 0x00028000... (61 %) + Writing at 0x0002c000... (66 %) + Writing at 0x00030000... (72 %) + Writing at 0x00034000... (77 %) + Writing at 0x00038000... (83 %) + Writing at 0x0003c000... (88 %) + Writing at 0x00040000... (94 %) + Writing at 0x00044000... (100 %) + Wrote 396432 bytes (292339 compressed) at 0x00000000 in 13.0 seconds (effective 243.4 kbit/s)... + Hash of data verified. + + Leaving... + Hard resetting via RTS pin... + ``` + +

+
+ +9. Monitor the MCU (microcontroller) locally via the Serial Port. + + - Go to menu `Tools`, `Serial Monitor`. + + If you perform this step right away after uploading the sketch, the serial monitor will show an output similar to the following upon success: + + ```text + Connecting to WIFI SSID buckaroo + .......................WiFi connected, IP address: + 192.168.1.123 + Setting time using SNTP..............................done! + Current time: Thu May 28 02:55:05 2020 + Client ID: mydeviceid + Username: myiothub.azure-devices.net/mydeviceid/?api-version=2018-06-30&DeviceClientType=c%2F1.0.0 + Password: SharedAccessSignature sr=myiothub.azure-devices.net%2Fdevices%2Fmydeviceid&sig=placeholder-password&se=1590620105 + MQTT connecting ... connected. + ``` + +10. Monitor the telemetry messages sent to the Azure IoT Hub using the connection string for the policy name `iothubowner` found under "Shared access policies" on your IoT Hub in the Azure portal. + + ```bash + $ az iot hub monitor-events --login --device-id + ``` + +
Expected telemetry output: +

+ + ```bash + Starting event monitor, filtering on device: mydeviceid, use ctrl-c to stop... + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + ^CStopping event monitor... + ``` + +

+
+ +## Certificates - Important to know + +The Azure IoT service certificates presented during TLS negotiation shall be always validated, on the device, using the appropriate trusted root CA certificate(s). + +For the Node MCU ESP8266 sample, our script `generate_arduino_zip_library.sh` automatically downloads the root certificate used in the United States regions (Baltimore CA certificate) and adds it to the Arduino sketch project. + +For other regions (and private cloud environments), please use the appropriate root CA certificate. + +### Additional Information + +For important information and additional guidance about certificates, please refer to [this blog post](https://techcommunity.microsoft.com/t5/internet-of-things/azure-iot-tls-changes-are-coming-and-why-you-should-care/ba-p/1658456) from the security team. + +## Troubleshooting + +- The error policy for the Embedded C SDK client library is documented [here](https://github.com/Azure/azure-sdk-for-c/blob/main/sdk/docs/iot/mqtt_state_machine.md#error-policy). +- File an issue via [GitHub Issues](https://github.com/Azure/azure-sdk-for-c/issues/new/choose). +- Check [previous questions](https://stackoverflow.com/questions/tagged/azure+c) or ask new ones on StackOverflow using the `azure` and `c` tags. + +## Contributing + +This project welcomes contributions and suggestions. Find more contributing details [here](https://github.com/Azure/azure-sdk-for-c/blob/main/CONTRIBUTING.md). + +### License + +This Azure SDK for C Arduino library is licensed under [MIT](https://github.com/Azure/azure-sdk-for-c-arduino/blob/main/LICENSE) license. + +Azure SDK for Embedded C is licensed under the [MIT](https://github.com/Azure/azure-sdk-for-c/blob/main/LICENSE) license. diff --git a/examples/Azure_IoT_Hub_RealtekAmebaD/Azure_IoT_Hub_RealtekAmebaD.ino b/examples/Azure_IoT_Hub_RealtekAmebaD/Azure_IoT_Hub_RealtekAmebaD.ino new file mode 100644 index 00000000..71f1ecb9 --- /dev/null +++ b/examples/Azure_IoT_Hub_RealtekAmebaD/Azure_IoT_Hub_RealtekAmebaD.ino @@ -0,0 +1,393 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +// C99 libraries +#include +#include +#include + +// Libraries for NTP, MQTT client, WiFi connection and SAS-token generation. +#include +#include +#include +#include +#include +#include + +// Azure IoT SDK for C includes +#include +#include +#include + +// Additional sample headers +#include "iot_configs.h" + +// When developing for your own Arduino-based platform, +// please follow the format '(ard;)'. +#define AZURE_SDK_CLIENT_USER_AGENT "c/" AZ_SDK_VERSION_STRING "(ard;amebaD)" + +// Utility macros and defines +// Status LED: will remain high on error and pulled high for a short time for each successful send. +#define LED_PIN 2 +#define sizeofarray(a) (sizeof(a) / sizeof(a[0])) +#define MQTT_PACKET_SIZE 1024 + +// Translate iot_configs.h defines into variables used by the sample +static const char* ssid = IOT_CONFIG_WIFI_SSID; +static const char* password = IOT_CONFIG_WIFI_PASSWORD; +static const char* host = IOT_CONFIG_IOTHUB_FQDN; +static const int mqtt_port = 8883; + +// Memory allocated for the sample's variables and structures. +static WiFiUDP ntp_udp_client; +static WiFiSSLClient wifi_client; +static PubSubClient mqtt_client(wifi_client); +static az_iot_hub_client hub_client; +static char sas_token[200]; +static size_t sas_token_length; + +static uint8_t signature[512]; +static unsigned long next_telemetry_send_time_ms = 0; +static char telemetry_topic[128]; +static uint8_t telemetry_payload[100]; +static uint32_t telemetry_send_count = 0; +static unsigned char* ca_pem_nullterm; + +// Auxiliary functions +extern "C"{ + extern int mbedtls_base64_decode( unsigned char *dst, size_t dlen, size_t *olen, const unsigned char *src, size_t slen); + extern int rom_hmac_sha256(const u8 *key, size_t key_len, const u8 *data, size_t data_len, u8 *mac); + extern int mbedtls_base64_encode( unsigned char *dst, size_t dlen, size_t *olen, const unsigned char *src, size_t slen); +} +static void createNullTerminatedRootCert() +{ + ca_pem_nullterm = (unsigned char*)malloc(ca_pem_len + 1); + + if (ca_pem_nullterm == NULL) + { + Serial.println("Failed allocating memory for null-terminated root ca"); + } + else + { + memcpy(ca_pem_nullterm, ca_pem, ca_pem_len); + ca_pem_nullterm[ca_pem_len] = '\0'; + } +} + + +static void connectToWiFi() +{ + Serial.begin(115200); + Serial.println(); + + Serial.print("Connecting to WIFI SSID "); + Serial.println(ssid); + + WiFi.begin((char*)ssid, (char*)password); + while (WiFi.status() != WL_CONNECTED) + { + delay(500); + Serial.print("."); + } + + Serial.println(""); + Serial.print("WiFi connected, IP address: "); + Serial.println(WiFi.localIP()); +} + +void receivedCallback(char* topic, byte* payload, unsigned int length) +{ + Serial.print("Received ["); + Serial.print(topic); + Serial.print("]: "); + for (unsigned int i = 0; i < length; i++) + { + Serial.print((char)payload[i]); + } + Serial.println(""); +} + +static void initializeClients() +{ + Serial.println("Initializing MQTT client"); + + az_iot_hub_client_options options = az_iot_hub_client_options_default(); + options.user_agent = AZ_SPAN_FROM_STR(AZURE_SDK_CLIENT_USER_AGENT); + + wifi_client.setRootCA(ca_pem_nullterm); + + if (az_result_failed(az_iot_hub_client_init( + &hub_client, + az_span_create((uint8_t*)host, strlen(host)), + AZ_SPAN_FROM_STR(IOT_CONFIG_DEVICE_ID), + &options))) + { + Serial.println("Failed initializing Azure IoT Hub client"); + return; + } + + mqtt_client.setServer(host, mqtt_port); + mqtt_client.setCallback(receivedCallback); + + Serial.println("MQTT client initialized"); +} + +int64_t iot_sample_get_epoch_expiration_time_from_minutes(uint32_t minutes) +{ + + long ts = 0; + long tus = 0; + unsigned int ttk = 0; + + //it should be ok to do init more than one time. It'll handle inside sntp_init(). + sntp_init(); + + sntp_get_lasttime(&ts, &tus, &ttk); + + while(ts == 0){ + vTaskDelay(1000 / portTICK_RATE_MS); + sntp_get_lasttime(&ts, &tus, &ttk); + } + + return (int64_t)ts + minutes * 60; +} + +static void hmac_sha256_sign_signature( + az_span decoded_key, + az_span signature, + az_span signed_signature, + az_span* out_signed_signature) +{ + if(rom_hmac_sha256( + az_span_ptr(decoded_key), + (size_t)az_span_size(decoded_key), + az_span_ptr(signature), + (size_t)az_span_size(signature), + az_span_ptr(signed_signature)) != 0) + + { + Serial.println("[ERROR] rom_hmac_sha256 failed"); + } + + *out_signed_signature = az_span_create(az_span_ptr(signed_signature), 32); +} + +static void base64_encode_bytes( + az_span decoded_bytes, + az_span base64_encoded_bytes, + az_span* out_base64_encoded_bytes) +{ + size_t len; + if(mbedtls_base64_encode(az_span_ptr(base64_encoded_bytes), (size_t)az_span_size(base64_encoded_bytes), + &len, az_span_ptr(decoded_bytes), (size_t)az_span_size(decoded_bytes)) != 0) + { + Serial.println("[ERROR] mbedtls_base64_encode fail"); + } + + *out_base64_encoded_bytes = az_span_create(az_span_ptr(base64_encoded_bytes), (int32_t)len); +} + +static void decode_base64_bytes( + az_span base64_encoded_bytes, + az_span decoded_bytes, + az_span* out_decoded_bytes) +{ + + memset(az_span_ptr(decoded_bytes), 0, (size_t)az_span_size(decoded_bytes)); + + size_t len; + if( mbedtls_base64_decode( az_span_ptr(decoded_bytes), (size_t)az_span_size(decoded_bytes), + &len, az_span_ptr(base64_encoded_bytes), (size_t)az_span_size(base64_encoded_bytes)) != 0) + { + Serial.println("[ERROR] mbedtls_base64_decode fail"); + } + + *out_decoded_bytes = az_span_create(az_span_ptr(decoded_bytes), (int32_t)len); +} + +static void iot_sample_generate_sas_base64_encoded_signed_signature( + az_span sas_base64_encoded_key, + az_span sas_signature, + az_span sas_base64_encoded_signed_signature, + az_span* out_sas_base64_encoded_signed_signature) +{ + // Decode the sas base64 encoded key to use for HMAC signing. + char sas_decoded_key_buffer[32]; + az_span sas_decoded_key = AZ_SPAN_FROM_BUFFER(sas_decoded_key_buffer); + decode_base64_bytes(sas_base64_encoded_key, sas_decoded_key, &sas_decoded_key); + + // HMAC-SHA256 sign the signature with the decoded key. + char sas_hmac256_signed_signature_buffer[32]; + az_span sas_hmac256_signed_signature = AZ_SPAN_FROM_BUFFER(sas_hmac256_signed_signature_buffer); + hmac_sha256_sign_signature(sas_decoded_key, sas_signature, sas_hmac256_signed_signature, &sas_hmac256_signed_signature); + + // Base64 encode the result of the HMAC signing. + base64_encode_bytes( + sas_hmac256_signed_signature, + sas_base64_encoded_signed_signature, + out_sas_base64_encoded_signed_signature); +} + +static void generate_sas_key(void) +{ + az_result rc; + // Create the POSIX expiration time from input minutes. + uint64_t sas_duration = iot_sample_get_epoch_expiration_time_from_minutes(SAS_TOKEN_EXPIRY_IN_MINUTES); + + + // Get the signature that will later be signed with the decoded key. + az_span sas_signature = AZ_SPAN_FROM_BUFFER(signature); + rc = az_iot_hub_client_sas_get_signature( + &hub_client, sas_duration, sas_signature, &sas_signature); + if (az_result_failed(rc)) + { + Serial.print("Could not get the signature for SAS key: az_result return code "); + Serial.println(rc); + } + + // Generate the encoded, signed signature (b64 encoded, HMAC-SHA256 signing). + char b64enc_hmacsha256_signature[64]; + az_span sas_base64_encoded_signed_signature = AZ_SPAN_FROM_BUFFER(b64enc_hmacsha256_signature); + iot_sample_generate_sas_base64_encoded_signed_signature( + AZ_SPAN_FROM_STR(IOT_CONFIG_DEVICE_KEY), + sas_signature, + sas_base64_encoded_signed_signature, + &sas_base64_encoded_signed_signature); + + // Get the resulting MQTT password, passing the base64 encoded, HMAC signed bytes. + size_t mqtt_password_length; + rc = az_iot_hub_client_sas_get_password( + &hub_client, + sas_duration, + sas_base64_encoded_signed_signature, + AZ_SPAN_EMPTY, + sas_token, + sizeof(sas_token), + &sas_token_length); + if (az_result_failed(rc)) + { + Serial.print("Could not get the password: az_result return code "); + Serial.println(rc); + } +} + +static int connect_to_azure_iot_hub() +{ + size_t client_id_length; + char mqtt_client_id[128]; + if (az_result_failed(az_iot_hub_client_get_client_id( + &hub_client, mqtt_client_id, sizeof(mqtt_client_id) - 1, &client_id_length))) + { + Serial.println("[ERROR] Failed getting client id"); + return 1; + } + + char mqtt_username[128]; + // Get the MQTT user name used to connect to IoT Hub + if (az_result_failed(az_iot_hub_client_get_user_name( + &hub_client, mqtt_username, sizeofarray(mqtt_username), NULL))) + { + printf("[ERROR] Failed to get MQTT clientId, return code\n"); + return 1; + } + + Serial.print("Client ID: "); + Serial.println(mqtt_client_id); + + Serial.print("Username: "); + Serial.println(mqtt_username); + + while (!mqtt_client.connected()) + { + Serial.print("MQTT connecting ... "); + + if (mqtt_client.connect(mqtt_client_id, mqtt_username, sas_token)) + { + Serial.println("connected."); + } + else + { + Serial.print("[ERROR] failed, status code ="); + Serial.print(mqtt_client.state()); + Serial.println(". Trying again in 5 seconds."); + // Wait 5 seconds before retrying + delay(5000); + } + } + + mqtt_client.subscribe(AZ_IOT_HUB_CLIENT_C2D_SUBSCRIBE_TOPIC); + + return 0; +} + +void establishConnection() +{ + connectToWiFi(); + + initializeClients(); + + generate_sas_key(); + + connect_to_azure_iot_hub(); + + digitalWrite(LED_PIN, LOW); +} + +static char* get_telemetry_payload() +{ + az_span temp_span = az_span_create(telemetry_payload, sizeof(telemetry_payload)); + temp_span = az_span_copy(temp_span, AZ_SPAN_FROM_STR("{ \"msgCount\": ")); + (void)az_span_u32toa(temp_span, telemetry_send_count++, &temp_span); + temp_span = az_span_copy(temp_span, AZ_SPAN_FROM_STR(" }")); + temp_span = az_span_copy_u8(temp_span, '\0'); + + return (char*)telemetry_payload; +} + +static void send_telemetry() +{ + digitalWrite(LED_PIN, HIGH); + Serial.print(millis()); + Serial.print(" Realtek Ameba-D Sending telemetry . . . "); + if (az_result_failed(az_iot_hub_client_telemetry_get_publish_topic( + &hub_client, NULL, telemetry_topic, sizeof(telemetry_topic), NULL))) + { + Serial.println("Failed az_iot_hub_client_telemetry_get_publish_topic"); + return; + } + + mqtt_client.publish(telemetry_topic, get_telemetry_payload(), false); + Serial.println("OK"); + delay(100); + digitalWrite(LED_PIN, LOW); +} + + +// Arduino setup and loop main functions. + +void setup() +{ + pinMode(LED_PIN, OUTPUT); + digitalWrite(LED_PIN, HIGH); + createNullTerminatedRootCert(); + establishConnection(); +} + +void loop() +{ + if (millis() > next_telemetry_send_time_ms) + { + // Check if connected, reconnect if needed. + if(!mqtt_client.connected()) + { + establishConnection(); + } + + send_telemetry(); + next_telemetry_send_time_ms = millis() + TELEMETRY_FREQUENCY_MILLISECS; + } + + // MQTT loop must be called to process Device-to-Cloud and Cloud-to-Device. + mqtt_client.loop(); + delay(500); +} diff --git a/examples/Azure_IoT_Hub_RealtekAmebaD/iot_configs.h b/examples/Azure_IoT_Hub_RealtekAmebaD/iot_configs.h new file mode 100644 index 00000000..5bd69a78 --- /dev/null +++ b/examples/Azure_IoT_Hub_RealtekAmebaD/iot_configs.h @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +// Wifi +#define IOT_CONFIG_WIFI_SSID "SSID" +#define IOT_CONFIG_WIFI_PASSWORD "PWD" + +// Azure IoT +#define IOT_CONFIG_IOTHUB_FQDN "[your Azure IoT host name].azure-devices.net" +#define IOT_CONFIG_DEVICE_ID "Device ID" +#define IOT_CONFIG_DEVICE_KEY "Device Key" + +// Publish 1 message every 2 seconds +#define TELEMETRY_FREQUENCY_MILLISECS 2000 + +// How long a SAS token generated by the sample will be valid, in minutes. +// The sample will stop working after the SAS token is expired, requiring the device to be reset. +#define SAS_TOKEN_EXPIRY_IN_MINUTES 60 diff --git a/examples/Azure_IoT_Hub_RealtekAmebaD/readme.md b/examples/Azure_IoT_Hub_RealtekAmebaD/readme.md new file mode 100644 index 00000000..deeac9e2 --- /dev/null +++ b/examples/Azure_IoT_Hub_RealtekAmebaD/readme.md @@ -0,0 +1,206 @@ +--- +page_type: sample +description: Connecting a Realtek Ameba D device to Azure IoT using the Azure SDK for Embedded C +languages: +- c +products: +- azure-iot +- azure-iot-pnp +- azure-iot-dps +- azure-iot-hub +--- + +# How to Setup and Run Azure SDK for Embedded C IoT Hub Client on Realtek AmebaD + +- [How to Setup and Run Azure SDK for Embedded C IoT Hub Client on Realtek AmebaD](#how-to-setup-and-run-azure-sdk-for-embedded-c-iot-hub-client-on-realtek-amebad) + - [Introduction](#introduction) + - [What is Covered](#what-is-covered) + - [Prerequisites](#prerequisites) + - [Setup and Run Instructions](#setup-and-run-instructions) + - [Certificates - Important to know](#certificates---important-to-know) + - [Additional Information](#additional-information) + - [Troubleshooting](#troubleshooting) + - [Contributing](#contributing) + - [License](#license) + +## Introduction + +This is a guide outlining how to run an Azure SDK for Embedded C IoT Hub telemetry sample on an Realtek AmebaD development board. + +### What is Covered + +- Configuration instructions for the Arduino IDE to compile a sample using the Azure SDK for Embedded C. +- Configuration, build, and run instructions for the IoT Hub telemetry sample. + +_The following was run on Windows 10 and Ubuntu Desktop 20.04 environments, with Arduino IDE 1.8.12 and Realtek Boards module 3.0.7._ + +## Prerequisites + +- Have an [Azure account](https://azure.microsoft.com/) created. +- Have an [Azure IoT Hub](https://docs.microsoft.com/azure/iot-hub/iot-hub-create-through-portal) created. +- Have a [logical device](https://docs.microsoft.com/azure/iot-hub/iot-hub-create-through-portal#register-a-new-device-in-the-iot-hub) created in your Azure IoT Hub using the authentication type "Symmetric Key". + + NOTE: Device keys are used to automatically generate a SAS token for authentication, which is only valid for one hour. + +- Have the latest [Arduino IDE](https://www.arduino.cc/en/Main/Software) installed. + +- [Install the USB](https://www.amebaiot.com/en/ameba-arduino-getting-started/) drivers for the Realtek AmebaD board. + + - You might need to install a USB driver directly from https://www.ftdichip.com/Drivers/VCP.htm + +- Have the [Realtek AmebaD board packages](https://www.amebaiot.com/en/amebad-arduino-getting-started/) installed on Arduino IDE. Realtek boards are not natively supported by Arduino IDE, so you need to add them manually. + + - Realtek boards are not natively supported by Arduino IDE, so you need to add them manually. + - Follow the [instructions](https://www.amebaiot.com/en/amebad-arduino-getting-started/) in the official Realtek AmebaD page. + +- Have one of the following interfaces to your Azure IoT Hub set up: + - [Azure Command Line Interface](https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest) (Azure CLI) utility installed, along with the [Azure IoT CLI extension](https://github.com/Azure/azure-iot-cli-extension). + + On Windows: + + Download and install: https://aka.ms/installazurecliwindows + + ```powershell + PS C:\>az extension add --name azure-iot + ``` + + On Linux: + + ```bash + $ curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + $ az extension add --name azure-iot + ``` + + A list of all the Azure IoT CLI extension commands can be found [here](https://docs.microsoft.com/cli/azure/iot?view=azure-cli-latest). + + - The most recent version of [Azure IoT Explorer](https://github.com/Azure/azure-iot-explorer/releases) installed. More instruction on its usage can be found [here](https://docs.microsoft.com/azure/iot-pnp/howto-use-iot-explorer). + + NOTE: This guide demonstrates use of the Azure CLI and does NOT demonstrate use of Azure IoT Explorer. + +## Setup and Run Instructions + +1. Run the Arduino IDE. + +2. Install the Azure SDK for Embedded C library. + + - On the Arduino IDE, go to menu `Sketch`, `Include Library`, `Manage Libraries...`. + - Search for and install `azure-sdk-for-c`. + +3. Open the Realtek AmebaD sample. + + - On the Arduino IDE, go to menu `File`, `Examples`, `azure-sdk-for-c`. + - Click on `az_realtek_amebaD` to open the sample. + +4. Configure the Realtek AmebaD sample. + + Enter your Azure IoT Hub and device information into the sample's `iot_configs.h`. + +5. Connect the Realtek AmebaD board to your USB port. + +6. On the Arduino IDE, select the board and port. + + - Go to menu `Tools`, `Board` and select `Ameba ARM (32-bits) Boards`/`RTL8722DM/RTL8722CSM`. + - Go to menu `Tools`, `Port` and select the port to which the microcontroller is connected. + +7. Upload the sketch. + + - Go to menu `Sketch` and click on `Upload`. + +8. Monitor the MCU (microcontroller) locally via the Serial Port. + + - Go to menu `Tools`, `Serial Monitor`. + + If you perform this step right away after uploading the sketch, the serial monitor will show an output similar to the following upon success: + + ```text + Connecting to WIFI SSID buckaroo + .......................WiFi connected, IP address: + 192.168.1.123 + Setting time using SNTP..............................done! + Current time: Thu May 28 02:55:05 2020 + Client ID: mydeviceid + Username: myiothub.azure-devices.net/mydeviceid/?api-version=2018-06-30&DeviceClientType=c%2F1.0.0 + Password: SharedAccessSignature sr=myiothub.azure-devices.net%2Fdevices%2Fmydeviceid&sig=placeholder-password&se=1590620105 + MQTT connecting ... connected. + ``` + +9. Monitor the telemetry messages sent to the Azure IoT Hub using the connection string for the policy name `iothubowner` found under "Shared access policies" on your IoT Hub in the Azure portal. + + ```bash + $ az iot hub monitor-events --login --device-id + ``` + +
Expected telemetry output: +

+ + ```bash + Starting event monitor, filtering on device: mydeviceid, use ctrl-c to stop... + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + { + "event": { + "origin": "mydeviceid", + "payload": "payload" + } + } + ^CStopping event monitor... + ``` + +

+
+ +## Certificates - Important to know + +The Azure IoT service certificates presented during TLS negotiation shall always be validated, on the device, using the appropriate trusted root CA certificate(s). + +For the Realtek AmebaD sample, our script `generate_arduino_zip_library.sh` automatically downloads the root certificate used in the United States regions (Baltimore CA certificate) and adds it to the Arduino sketch project. + +For other regions (and private cloud environments), please use the appropriate root CA certificate. + +### Additional Information + +For important information and additional guidance about certificates, please refer to [this blog post](https://techcommunity.microsoft.com/t5/internet-of-things/azure-iot-tls-changes-are-coming-and-why-you-should-care/ba-p/1658456) from the security team. + +## Troubleshooting + +- The error policy for the Embedded C SDK client library is documented [here](https://github.com/Azure/azure-sdk-for-c/blob/main/sdk/docs/iot/mqtt_state_machine.md#error-policy). +- File an issue via [Github Issues](https://github.com/Azure/azure-sdk-for-c/issues/new/choose). +- Check [previous questions](https://stackoverflow.com/questions/tagged/azure+c) or ask new ones on StackOverflow using the `azure` and `c` tags. + +## Contributing + +This project welcomes contributions and suggestions. Find more contributing details [here](https://github.com/Azure/azure-sdk-for-c/blob/main/CONTRIBUTING.md). + +### License + +This Azure SDK for C Arduino library is licensed under [MIT](https://github.com/Azure/azure-sdk-for-c-arduino/blob/main/LICENSE) license. + +Azure SDK for Embedded C is licensed under the [MIT](https://github.com/Azure/azure-sdk-for-c/blob/main/LICENSE) license. diff --git a/library.properties b/library.properties new file mode 100644 index 00000000..1b1cfa75 --- /dev/null +++ b/library.properties @@ -0,0 +1,10 @@ +name=Azure SDK for C +version=1.0.0-beta.1 +author=Microsoft Corporation +maintainer=Microsoft Azure IoT +sentence=Azure SDK for C library (1.3.0-beta.1) for Arduino. +paragraph=Arduino port of the Azure SDK for C. It allows you to use your Arduino device with Azure services like Azure IoT Hub and Azure Device Provisioning Service. See README.md for more details. Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information. +category=Communication +url=https://github.com/Azure/azure-sdk-for-c/tree/1.3.0-beta.1 +architectures=* +includes=az_core.h,az_iot.h,azure_ca.h diff --git a/src/_az_cfg.h b/src/_az_cfg.h new file mode 100644 index 00000000..41b5b234 --- /dev/null +++ b/src/_az_cfg.h @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Warnings configuration and common macros for Azure SDK code. + * Do not include this file directly. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifdef _MSC_VER + +// Disable warnings: +// ----------------- + +// warning C4204: nonstandard extension used: non-constant aggregate initializer +#pragma warning(disable : 4204) + +// warning C4221: nonstandard extension used: '...': cannot be initialized using address of +// automatic variable '...' +#pragma warning(disable : 4221) + +// warning C28278 : Function appears with no prototype in scope. Only limited analysis can be +// performed. Include the appropriate header or add a prototype. This warning also occurs if +// parameter or return types are omitted in a function definition. +#pragma warning(disable : 28278) + +// Treat warnings as errors: +// ------------------------- + +// warning C4710: '...': function not inlined +#pragma warning(error : 4710) + +#endif // _MSC_VER + +#ifdef __GNUC__ + +#pragma GCC diagnostic ignored "-Wmissing-braces" + +#endif // __GNUC__ + +#ifdef __clang__ + +#pragma clang diagnostic ignored "-Wmissing-field-initializers" +#pragma clang diagnostic ignored "-Wmissing-braces" + +#endif // __clang__ + +#ifndef _az_CFG_H +#define _az_CFG_H + +/** + * @brief Inline function. + */ +#ifdef _MSC_VER +#define AZ_INLINE static __forceinline +#elif defined(__GNUC__) || defined(__clang__) // !_MSC_VER +#define AZ_INLINE __attribute__((always_inline)) static inline +#else // !_MSC_VER !__GNUC__ !__clang__ +#define AZ_INLINE static inline +#endif // _MSC_VER + +#if defined(__GNUC__) && __GNUC__ >= 7 +#define _az_FALLTHROUGH __attribute__((fallthrough)) +#else // !__GNUC__ >= 7 +#define _az_FALLTHROUGH \ + do \ + { \ + } while (0) +#endif // __GNUC__ >= 7 + +/** + * @brief Enforce that the return value is handled (only applicable on supported compilers). + */ +#ifdef _MSC_VER +#define AZ_NODISCARD _Check_return_ +#elif defined(__GNUC__) || defined(__clang__) // !_MSC_VER +#define AZ_NODISCARD __attribute__((warn_unused_result)) +#else // !_MSC_VER !__GNUC__ !__clang__ +#define AZ_NODISCARD +#endif // _MSC_VER + +// Get the number of elements in an array +#define _az_COUNTOF(array) (sizeof(array) / sizeof((array)[0])) + +#endif // _az_CFG_H diff --git a/src/_az_cfg_prefix.h b/src/_az_cfg_prefix.h new file mode 100644 index 00000000..369cdc42 --- /dev/null +++ b/src/_az_cfg_prefix.h @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Opens "extern C" and saves warnings state. + * Do not include this file directly. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifdef __cplusplus +extern "C" +{ +#endif // __cplusplus + +#ifdef _MSC_VER +#pragma warning(push) +#elif defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6)) // !_MSC_VER +#pragma GCC diagnostic push +#elif defined(__clang__) // !_MSC_VER !__clang__ +#pragma clang diagnostic push +#endif // _MSC_VER + +#include <_az_cfg.h> diff --git a/src/_az_cfg_suffix.h b/src/_az_cfg_suffix.h new file mode 100644 index 00000000..5c53db36 --- /dev/null +++ b/src/_az_cfg_suffix.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Closes "extern C" and restores warnings state. + * Do not include this file directly. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifdef _MSC_VER +#pragma warning(pop) +#elif defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6)) // !_MSC_VER +#pragma GCC diagnostic pop +#elif defined(__clang__) // !_MSC_VER !__GNUC__ +#pragma clang diagnostic pop // NOLINT(clang-diagnostic-unknown-pragmas) +#endif + +#ifdef __cplusplus +} +#endif // __cplusplus diff --git a/src/az_base64.c b/src/az_base64.c new file mode 100644 index 00000000..99f2531d --- /dev/null +++ b/src/az_base64.c @@ -0,0 +1,521 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include +#include + +#include <_az_cfg.h> + +// The maximum integer length of binary data that can be encoded into base 64 text and still fit +// into an az_span which has an int32_t length, i.e. (INT32_MAX / 4) * 3; +#define _az_MAX_SAFE_ENCODED_LENGTH 1610612733 + +#define _az_ENCODING_PAD '=' + +static char const _az_base64_encode_array[65] + = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static int8_t const _az_base64_decode_array[256] = { + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 62, + -1, + -1, + -1, + 63, // 62 is placed at index 43 (for +), 63 at index 47 (for /) + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + -1, + -1, + -1, + -1, + -1, + -1, // 52-61 are placed at index 48-57 (for 0-9), 64 at index 61 (for =) + -1, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + -1, + -1, + -1, + -1, + -1, // 0-25 are placed at index 65-90 (for A-Z) + -1, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + -1, + -1, + -1, + -1, + -1, // 26-51 are placed at index 97-122 (for a-z) + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, // Bytes over 122 ('z') are invalid and cannot be decoded. Hence, padding the map with 255, + // which indicates invalid input + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, +}; + +static AZ_NODISCARD int32_t _az_base64_encode(uint8_t* three_bytes) +{ + int32_t i = (*three_bytes << 16) | (*(three_bytes + 1) << 8) | *(three_bytes + 2); + + int32_t i0 = _az_base64_encode_array[i >> 18]; + int32_t i1 = _az_base64_encode_array[(i >> 12) & 0x3F]; + int32_t i2 = _az_base64_encode_array[(i >> 6) & 0x3F]; + int32_t i3 = _az_base64_encode_array[i & 0x3F]; + + return i0 | (i1 << 8) | (i2 << 16) | (i3 << 24); +} + +static AZ_NODISCARD int32_t _az_base64_encode_and_pad_one(uint8_t* two_bytes) +{ + int32_t i = (*two_bytes << 16) | (*(two_bytes + 1) << 8); + + int32_t i0 = _az_base64_encode_array[i >> 18]; + int32_t i1 = _az_base64_encode_array[(i >> 12) & 0x3F]; + int32_t i2 = _az_base64_encode_array[(i >> 6) & 0x3F]; + + return i0 | (i1 << 8) | (i2 << 16) | (_az_ENCODING_PAD << 24); +} + +static AZ_NODISCARD int32_t _az_base64_encode_and_pad_two(uint8_t* one_byte) +{ + int32_t i = (*one_byte << 8); + + int32_t i0 = _az_base64_encode_array[i >> 10]; + int32_t i1 = _az_base64_encode_array[(i >> 4) & 0x3F]; + + return i0 | (i1 << 8) | (_az_ENCODING_PAD << 16) | (_az_ENCODING_PAD << 24); +} + +static void _az_base64_write_int_as_four_bytes(uint8_t* destination, int32_t value) +{ + *(destination + 3) = (uint8_t)((value >> 24) & 0xFF); + *(destination + 2) = (uint8_t)((value >> 16) & 0xFF); + *(destination + 1) = (uint8_t)((value >> 8) & 0xFF); + *(destination + 0) = (uint8_t)(value & 0xFF); +} + +AZ_NODISCARD az_result +az_base64_encode(az_span destination_base64_text, az_span source_bytes, int32_t* out_written) +{ + _az_PRECONDITION_VALID_SPAN(destination_base64_text, 4, false); + _az_PRECONDITION_VALID_SPAN(source_bytes, 1, false); + _az_PRECONDITION_NOT_NULL(out_written); + + int32_t source_length = az_span_size(source_bytes); + uint8_t* source_ptr = az_span_ptr(source_bytes); + + int32_t destination_length = az_span_size(destination_base64_text); + uint8_t* destination_ptr = az_span_ptr(destination_base64_text); + + if (destination_length < az_base64_get_max_encoded_size(source_length)) + { + return AZ_ERROR_NOT_ENOUGH_SPACE; + } + + int32_t source_index = 0; + int32_t result = 0; + + while (source_index < source_length - 2) + { + result = _az_base64_encode(source_ptr + source_index); + _az_base64_write_int_as_four_bytes(destination_ptr, result); + destination_ptr += 4; + source_index += 3; + } + + if (source_index == source_length - 1) + { + result = _az_base64_encode_and_pad_two(source_ptr + source_index); + _az_base64_write_int_as_four_bytes(destination_ptr, result); + destination_ptr += 4; + source_index += 1; + } + else if (source_index == source_length - 2) + { + result = _az_base64_encode_and_pad_one(source_ptr + source_index); + _az_base64_write_int_as_four_bytes(destination_ptr, result); + destination_ptr += 4; + source_index += 2; + } + + *out_written = (int32_t)(destination_ptr - az_span_ptr(destination_base64_text)); + return AZ_OK; +} + +AZ_NODISCARD int32_t az_base64_get_max_encoded_size(int32_t source_bytes_size) +{ + _az_PRECONDITION_RANGE(0, source_bytes_size, _az_MAX_SAFE_ENCODED_LENGTH); + return (((source_bytes_size + 2) / 3) * 4); +} + +static AZ_NODISCARD int32_t _az_base64_decode(uint8_t* encoded_bytes) +{ + int32_t i0 = *encoded_bytes; + int32_t i1 = *(encoded_bytes + 1); + int32_t i2 = *(encoded_bytes + 2); + int32_t i3 = *(encoded_bytes + 3); + + i0 = _az_base64_decode_array[i0]; + i1 = _az_base64_decode_array[i1]; + i2 = _az_base64_decode_array[i2]; + i3 = _az_base64_decode_array[i3]; + + i0 <<= 18; + i1 <<= 12; + i2 <<= 6; + + i0 |= i3; + i1 |= i2; + + i0 |= i1; + return i0; +} + +static void _az_base64_write_three_low_order_bytes(uint8_t* destination, int32_t value) +{ + *destination = (uint8_t)(value >> 16); + *(destination + 1) = (uint8_t)(value >> 8); + *(destination + 2) = (uint8_t)(value); +} + +AZ_NODISCARD az_result +az_base64_decode(az_span destination_bytes, az_span source_base64_text, int32_t* out_written) +{ + _az_PRECONDITION_VALID_SPAN(destination_bytes, 1, false); + _az_PRECONDITION_VALID_SPAN(source_base64_text, 4, false); + _az_PRECONDITION_NOT_NULL(out_written); + + int32_t source_length = az_span_size(source_base64_text); + uint8_t* source_ptr = az_span_ptr(source_base64_text); + + int32_t destination_length = az_span_size(destination_bytes); + uint8_t* destination_ptr = az_span_ptr(destination_bytes); + + // The input must be non-empty and a multiple of 4 to be valid. + if (source_length == 0 || source_length % 4 != 0) + { + return AZ_ERROR_UNEXPECTED_END; + } + + if (destination_length < az_base64_get_max_decoded_size(source_length) - 2) + { + return AZ_ERROR_NOT_ENOUGH_SPACE; + } + + int32_t source_index = 0; + int32_t destination_index = 0; + + while (source_index < source_length - 4) + { + int32_t result = _az_base64_decode(source_ptr + source_index); + if (result < 0) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + _az_base64_write_three_low_order_bytes(destination_ptr, result); + destination_ptr += 3; + destination_index += 3; + source_index += 4; + } + + // We are guaranteed to have an input with at least 4 bytes at this point, with a size that is a + // multiple of 4. + int32_t i0 = *(source_ptr + source_length - 4); + int32_t i1 = *(source_ptr + source_length - 3); + int32_t i2 = *(source_ptr + source_length - 2); + int32_t i3 = *(source_ptr + source_length - 1); + + i0 = _az_base64_decode_array[i0]; + i1 = _az_base64_decode_array[i1]; + + i0 <<= 18; + i1 <<= 12; + + i0 |= i1; + + if (i3 != _az_ENCODING_PAD) + { + i2 = _az_base64_decode_array[i2]; + i3 = _az_base64_decode_array[i3]; + + i2 <<= 6; + + i0 |= i3; + i0 |= i2; + + if (i0 < 0) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + if (destination_index > destination_length - 3) + { + return AZ_ERROR_NOT_ENOUGH_SPACE; + } + _az_base64_write_three_low_order_bytes(destination_ptr, i0); + destination_ptr += 3; + } + else if (i2 != _az_ENCODING_PAD) + { + i2 = _az_base64_decode_array[i2]; + + i2 <<= 6; + + i0 |= i2; + + if (i0 < 0) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + if (destination_index > destination_length - 2) + { + return AZ_ERROR_NOT_ENOUGH_SPACE; + } + *(destination_ptr + 1) = (uint8_t)(i0 >> 8); + *destination_ptr = (uint8_t)(i0 >> 16); + destination_ptr += 2; + } + else + { + if (i0 < 0) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + if (destination_index > destination_length - 1) + { + return AZ_ERROR_NOT_ENOUGH_SPACE; + } + *destination_ptr = (uint8_t)(i0 >> 16); + destination_ptr += 1; + } + + *out_written = (int32_t)(destination_ptr - az_span_ptr(destination_bytes)); + return AZ_OK; +} + +AZ_NODISCARD int32_t az_base64_get_max_decoded_size(int32_t source_base64_text_size) +{ + _az_PRECONDITION(source_base64_text_size >= 0); + return (source_base64_text_size / 4) * 3; +} diff --git a/src/az_base64.h b/src/az_base64.h new file mode 100644 index 00000000..dc80abfa --- /dev/null +++ b/src/az_base64.h @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Defines APIs to convert between binary data and UTF-8 encoded text that is represented in + * base 64. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_BASE64_H +#define _az_BASE64_H + +#include +#include + +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief Encodes the span of binary data into UTF-8 encoded text represented as base 64. + * + * @param destination_base64_text The output #az_span where the encoded base 64 text should be + * copied to as a result of the operation. + * @param[in] source_bytes The input #az_span that contains binary data to be encoded. + * @param[out] out_written A pointer to an `int32_t` that receives the number of bytes written into + * the destination #az_span. This can be used to slice the output for subsequent calls, if + * necessary. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The \p destination_base64_text is not large enough to contain + * the encoded bytes. + */ +AZ_NODISCARD az_result +az_base64_encode(az_span destination_base64_text, az_span source_bytes, int32_t* out_written); + +/** + * @brief Returns the maximum length of the result if you were to encode an #az_span of the + * specified length which contained binary data. + * + * @param source_bytes_size The size of the span containing binary data. + * + * @return The maximum length of the result. + */ +AZ_NODISCARD int32_t az_base64_get_max_encoded_size(int32_t source_bytes_size); + +/** + * @brief Decodes the span of UTF-8 encoded text represented as base 64 into binary data. + * + * @param destination_bytes The output #az_span where the decoded binary data should be copied to as + * a result of the operation. + * @param[in] source_base64_text The input #az_span that contains the base 64 text to be decoded. + * @param[out] out_written A pointer to an `int32_t` that receives the number of bytes written into + * the destination #az_span. This can be used to slice the output for subsequent calls, if + * necessary. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The \p destination_bytes is not large enough to contain + * the decoded text. + * @retval #AZ_ERROR_UNEXPECTED_CHAR The input \p source_base64_text contains characters outside of + * the expected base 64 range, has invalid or more than two padding characters, or is incomplete + * (that is, not a multiple of 4). + * @retval #AZ_ERROR_UNEXPECTED_END The input \p source_base64_text is incomplete (that is, it is + * not of a size which is a multiple of 4). + */ +AZ_NODISCARD az_result +az_base64_decode(az_span destination_bytes, az_span source_base64_text, int32_t* out_written); + +/** + * @brief Returns the maximum length of the result if you were to decode an #az_span of the + * specified length which contained base 64 encoded text. + * + * @param source_base64_text_size The size of the span containing base 64 encoded text. + * + * @return The maximum length of the result. + */ +AZ_NODISCARD int32_t az_base64_get_max_decoded_size(int32_t source_base64_text_size); + +#include <_az_cfg_suffix.h> + +#endif // _az_BASE64_H diff --git a/src/az_config.h b/src/az_config.h new file mode 100644 index 00000000..fbbe345e --- /dev/null +++ b/src/az_config.h @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Configurable constants used by the Azure SDK. + * + * @remarks Typically, these constants do not need to be modified but depending on how your + * application uses an Azure service, they can be adjusted. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_CONFIG_H +#define _az_CONFIG_H + +#include <_az_cfg_prefix.h> + +enum +{ + /// The maximum buffer size for a URL. + AZ_HTTP_REQUEST_URL_BUFFER_SIZE = 2 * 1024, + + /// The maximum buffer size for an HTTP request body. + AZ_HTTP_REQUEST_BODY_BUFFER_SIZE = 1024, + + /// The maximum buffer size for a log message. + AZ_LOG_MESSAGE_BUFFER_SIZE = 1024, +}; + +#include <_az_cfg_suffix.h> + +#endif // _az_CONFIG_H diff --git a/src/az_config_internal.h b/src/az_config_internal.h new file mode 100644 index 00000000..3128ad63 --- /dev/null +++ b/src/az_config_internal.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#ifndef _az_CONFIG_INTERNAL_H +#define _az_CONFIG_INTERNAL_H + +#include +#include + +#include + +#include <_az_cfg_prefix.h> + +enum +{ + _az_TIME_SECONDS_PER_MINUTE = 60, + _az_TIME_MILLISECONDS_PER_SECOND = 1000, + _az_TIME_MICROSECONDS_PER_MILLISECOND = 1000, +}; + +/* + * Int64 is max value 9223372036854775808 (19 characters) + * min value -9223372036854775808 (20 characters) + */ +enum +{ + _az_INT64_AS_STR_BUFFER_SIZE = 20, +}; + +#include <_az_cfg_suffix.h> + +#endif // _az_CONFIG_INTERNAL_H diff --git a/src/az_context.c b/src/az_context.c new file mode 100644 index 00000000..f3603877 --- /dev/null +++ b/src/az_context.c @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include +#include + +#include + +#include <_az_cfg.h> + +// This is a global az_context node representing the entire application. By default, this node +// never expires. Call az_context_cancel passing a pointer to this node to cancel the entire +// application (which cancels all the child nodes). +az_context az_context_application = { + ._internal + = { .parent = NULL, .expiration = _az_CONTEXT_MAX_EXPIRATION, .key = NULL, .value = NULL } +}; + +// Returns the soonest expiration time of this az_context node or any of its parent nodes. +AZ_NODISCARD int64_t az_context_get_expiration(az_context const* context) +{ + _az_PRECONDITION_NOT_NULL(context); + + int64_t expiration = _az_CONTEXT_MAX_EXPIRATION; + for (; context != NULL; context = context->_internal.parent) + { + if (context->_internal.expiration < expiration) + { + expiration = context->_internal.expiration; + } + } + return expiration; +} + +// Walks up this az_context node's parent until it find a node whose key matches the specified key +// and return the corresponding value. Returns AZ_ERROR_ITEM_NOT_FOUND is there are no nodes +// matching the specified key. +AZ_NODISCARD az_result +az_context_get_value(az_context const* context, void const* key, void const** out_value) +{ + _az_PRECONDITION_NOT_NULL(context); + _az_PRECONDITION_NOT_NULL(out_value); + _az_PRECONDITION_NOT_NULL(key); + + for (; context != NULL; context = context->_internal.parent) + { + if (context->_internal.key == key) + { + *out_value = context->_internal.value; + return AZ_OK; + } + } + *out_value = NULL; + return AZ_ERROR_ITEM_NOT_FOUND; +} + +AZ_NODISCARD az_context +az_context_create_with_expiration(az_context const* parent, int64_t expiration) +{ + _az_PRECONDITION_NOT_NULL(parent); + _az_PRECONDITION(expiration >= 0); + + return (az_context){ ._internal = { .parent = parent, .expiration = expiration } }; +} + +AZ_NODISCARD az_context +az_context_create_with_value(az_context const* parent, void const* key, void const* value) +{ + _az_PRECONDITION_NOT_NULL(parent); + _az_PRECONDITION_NOT_NULL(key); + + return (az_context){ + ._internal + = { .parent = parent, .expiration = _az_CONTEXT_MAX_EXPIRATION, .key = key, .value = value } + }; +} + +void az_context_cancel(az_context* ref_context) +{ + _az_PRECONDITION_NOT_NULL(ref_context); + + ref_context->_internal.expiration = 0; // The beginning of time +} + +AZ_NODISCARD bool az_context_has_expired(az_context const* context, int64_t current_time) +{ + _az_PRECONDITION_NOT_NULL(context); + _az_PRECONDITION(current_time >= 0); + + return az_context_get_expiration(context) < current_time; +} diff --git a/src/az_context.h b/src/az_context.h new file mode 100644 index 00000000..ab99419d --- /dev/null +++ b/src/az_context.h @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Context for canceling long running operations. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_CONTEXT_H +#define _az_CONTEXT_H + +#include + +#include +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief A context is a node within a tree that represents expiration times and key/value pairs. + */ +// Definition is below. Defining the typedef first is necessary here since there is a cycle. +typedef struct az_context az_context; + +/** + * @brief A context is a node within a tree that represents expiration times and key/value pairs. + * + * @details The root node in the tree (ultimate parent). + */ +struct az_context +{ + struct + { + az_context const* parent; // Pointer to parent context (or NULL); immutable after creation + int64_t expiration; // Time when context expires + void const* key; // Pointers to the key & value (usually NULL) + void const* value; + } _internal; +}; + +#define _az_CONTEXT_MAX_EXPIRATION 0x7FFFFFFFFFFFFFFF + +/** + * @brief The application root #az_context instances. + * @details The #az_context_application never expires but you can explicitly cancel it by passing + * its address to #az_context_cancel() which effectively cancels all its #az_context child nodes. + */ +extern az_context az_context_application; + +/** + * @brief Creates a new expiring #az_context node that is a child of the specified parent. + * + * @param[in] parent The #az_context node that is the parent to the new node. + * @param[in] expiration The time when this new node should be canceled. + * + * @return The new child #az_context node. + */ +AZ_NODISCARD az_context +az_context_create_with_expiration(az_context const* parent, int64_t expiration); + +/** + * @brief Creates a new key/value az_context node that is a child of the specified parent. + * + * @param[in] parent The #az_context node that is the parent to the new node. + * @param[in] key A pointer to the key of this new #az_context node. + * @param[in] value A pointer to the value of this new #az_context node. + * + * @return The new child #az_context node. + */ +AZ_NODISCARD az_context +az_context_create_with_value(az_context const* parent, void const* key, void const* value); + +/** + * @brief Cancels the specified #az_context node; this cancels all the child nodes as well. + * + * @param[in,out] ref_context A pointer to the #az_context node to be canceled. + */ +void az_context_cancel(az_context* ref_context); + +/** + * @brief Returns the soonest expiration time of this #az_context node or any of its parent nodes. + * + * @param[in] context A pointer to an #az_context node. + * @return The soonest expiration time from this context and its parents. + */ +AZ_NODISCARD int64_t az_context_get_expiration(az_context const* context); + +/** + * @brief Returns `true` if this #az_context node or any of its parent nodes' expiration is before + * the \p current_time. + * + * @param[in] context A pointer to the #az_context node to check. + * @param[in] current_time The current time. + */ +AZ_NODISCARD bool az_context_has_expired(az_context const* context, int64_t current_time); + +/** + * @brief Walks up this #az_context node's parents until it find a node whose key matches the + * specified key and returns the corresponding value. + * + * @param[in] context The #az_context node in the tree where checking starts. + * @param[in] key A pointer to the key to be scanned for. + * @param[out] out_value A pointer to a `void const*` that will receive the key's associated value + * if the key is found. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The key is found. + * @retval #AZ_ERROR_ITEM_NOT_FOUND No nodes are found with the specified key. + */ +AZ_NODISCARD az_result +az_context_get_value(az_context const* context, void const* key, void const** out_value); + +#include <_az_cfg_suffix.h> + +#endif // _az_CONTEXT_H diff --git a/src/az_core.h b/src/az_core.h new file mode 100644 index 00000000..bc78464a --- /dev/null +++ b/src/az_core.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Azure Core public headers. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_CORE_H +#define _az_CORE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#endif //_az_CORE_H diff --git a/src/az_credentials.h b/src/az_credentials.h new file mode 100644 index 00000000..8b849647 --- /dev/null +++ b/src/az_credentials.h @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Credentials used for authentication with many (not all) Azure SDK client libraries. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_CREDENTIALS_H +#define _az_CREDENTIALS_H + +#include +#include +#include + +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief Equivalent to no credential (`NULL`). + */ +#define AZ_CREDENTIAL_ANONYMOUS NULL + +/** + * @brief Function callback definition as a contract to be implemented for a credential to set + * authentication scopes when it is supported by the type of the credential. + */ +typedef AZ_NODISCARD az_result ( + *_az_credential_set_scopes_fn)(void* ref_credential, az_span scopes); + +/** + * @brief Credential definition. It is used internally to authenticate an SDK client with Azure. + * All types of credentials must contain this structure as their first member. + */ +typedef struct +{ + struct + { + _az_http_policy_process_fn apply_credential_policy; + + /// If the credential doesn't support scopes, this function pointer is `NULL`. + _az_credential_set_scopes_fn set_scopes; + } _internal; +} _az_credential; + +#include <_az_cfg_suffix.h> + +#endif // _az_CREDENTIALS_H diff --git a/src/az_credentials_internal.h b/src/az_credentials_internal.h new file mode 100644 index 00000000..2cb6c84f --- /dev/null +++ b/src/az_credentials_internal.h @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#ifndef _az_CREDENTIALS_INTERNAL_H +#define _az_CREDENTIALS_INTERNAL_H + +#include +#include +#include + +#include + +#include <_az_cfg_prefix.h> + +AZ_INLINE AZ_NODISCARD az_result +_az_credential_set_scopes(_az_credential* credential, az_span scopes) +{ + return (credential == NULL || credential->_internal.set_scopes == NULL) + ? AZ_OK + : (credential->_internal.set_scopes)(credential, scopes); +} + +#include <_az_cfg_suffix.h> + +#endif // _az_CREDENTIALS_INTERNAL_H diff --git a/src/az_hex_private.h b/src/az_hex_private.h new file mode 100644 index 00000000..fd798916 --- /dev/null +++ b/src/az_hex_private.h @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#ifndef _az_HEX_PRIVATE_H +#define _az_HEX_PRIVATE_H + +#include + +#include <_az_cfg_prefix.h> + +enum +{ + _az_HEX_LOWER_OFFSET = 'a' - 10, + _az_HEX_UPPER_OFFSET = 'A' - 10, +}; + +/** + * Converts a number [0..15] into uppercase hexadecimal digit character (base16). + */ +AZ_NODISCARD AZ_INLINE uint8_t _az_number_to_upper_hex(uint8_t number) +{ + // The 10 is one more than the last non-alphabetic digit of the hex values (0-9, A-F). + // Every value under 10 is a single digit, whereas values 10 and above are hex letters. + + // NOLINTNEXTLINE(readability-magic-numbers, cppcoreguidelines-avoid-magic-numbers) + return (uint8_t)(number + (number < 10 ? '0' : _az_HEX_UPPER_OFFSET)); +} + +#include <_az_cfg_suffix.h> + +#endif // _az_HEX_PRIVATE_H diff --git a/src/az_http.h b/src/az_http.h new file mode 100644 index 00000000..dfd81887 --- /dev/null +++ b/src/az_http.h @@ -0,0 +1,288 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief This header defines the types and functions your application uses to leverage HTTP request + * and response functionality. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_HTTP_H +#define _az_HTTP_H + +#include +#include +#include +#include + +#include +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief Defines the possible HTTP status codes. + */ +typedef enum +{ + /// No HTTP status code. + AZ_HTTP_STATUS_CODE_NONE = 0, + + // === 1xx (information) Status Codes: === + AZ_HTTP_STATUS_CODE_CONTINUE = 100, ///< HTTP 100 Continue. + AZ_HTTP_STATUS_CODE_SWITCHING_PROTOCOLS = 101, ///< HTTP 101 Switching Protocols. + AZ_HTTP_STATUS_CODE_PROCESSING = 102, ///< HTTP 102 Processing. + AZ_HTTP_STATUS_CODE_EARLY_HINTS = 103, ///< HTTP 103 Early Hints. + + // === 2xx (successful) Status Codes: === + AZ_HTTP_STATUS_CODE_OK = 200, ///< HTTP 200 OK. + AZ_HTTP_STATUS_CODE_CREATED = 201, ///< HTTP 201 Created. + AZ_HTTP_STATUS_CODE_ACCEPTED = 202, ///< HTTP 202 Accepted. + AZ_HTTP_STATUS_CODE_NON_AUTHORITATIVE_INFORMATION + = 203, ///< HTTP 203 Non-Authoritative Information. + AZ_HTTP_STATUS_CODE_NO_CONTENT = 204, ///< HTTP 204 No Content. + AZ_HTTP_STATUS_CODE_RESET_CONTENT = 205, ///< HTTP 205 Rest Content. + AZ_HTTP_STATUS_CODE_PARTIAL_CONTENT = 206, ///< HTTP 206 Partial Content. + AZ_HTTP_STATUS_CODE_MULTI_STATUS = 207, ///< HTTP 207 Multi-Status. + AZ_HTTP_STATUS_CODE_ALREADY_REPORTED = 208, ///< HTTP 208 Already Reported. + AZ_HTTP_STATUS_CODE_IM_USED = 226, ///< HTTP 226 IM Used. + + // === 3xx (redirection) Status Codes: === + AZ_HTTP_STATUS_CODE_MULTIPLE_CHOICES = 300, ///< HTTP 300 Multiple Choices. + AZ_HTTP_STATUS_CODE_MOVED_PERMANENTLY = 301, ///< HTTP 301 Moved Permanently. + AZ_HTTP_STATUS_CODE_FOUND = 302, ///< HTTP 302 Found. + AZ_HTTP_STATUS_CODE_SEE_OTHER = 303, ///< HTTP 303 See Other. + AZ_HTTP_STATUS_CODE_NOT_MODIFIED = 304, ///< HTTP 304 Not Modified. + AZ_HTTP_STATUS_CODE_USE_PROXY = 305, ///< HTTP 305 Use Proxy. + AZ_HTTP_STATUS_CODE_TEMPORARY_REDIRECT = 307, ///< HTTP 307 Temporary Redirect. + AZ_HTTP_STATUS_CODE_PERMANENT_REDIRECT = 308, ///< HTTP 308 Permanent Redirect. + + // === 4xx (client error) Status Codes: === + AZ_HTTP_STATUS_CODE_BAD_REQUEST = 400, ///< HTTP 400 Bad Request. + AZ_HTTP_STATUS_CODE_UNAUTHORIZED = 401, ///< HTTP 401 Unauthorized. + AZ_HTTP_STATUS_CODE_PAYMENT_REQUIRED = 402, ///< HTTP 402 Payment Required. + AZ_HTTP_STATUS_CODE_FORBIDDEN = 403, ///< HTTP 403 Forbidden. + AZ_HTTP_STATUS_CODE_NOT_FOUND = 404, ///< HTTP 404 Not Found. + AZ_HTTP_STATUS_CODE_METHOD_NOT_ALLOWED = 405, ///< HTTP 405 Method Not Allowed. + AZ_HTTP_STATUS_CODE_NOT_ACCEPTABLE = 406, ///< HTTP 406 Not Acceptable. + AZ_HTTP_STATUS_CODE_PROXY_AUTHENTICATION_REQUIRED + = 407, ///< HTTP 407 Proxy Authentication Required. + AZ_HTTP_STATUS_CODE_REQUEST_TIMEOUT = 408, ///< HTTP 408 Request Timeout. + AZ_HTTP_STATUS_CODE_CONFLICT = 409, ///< HTTP 409 Conflict. + AZ_HTTP_STATUS_CODE_GONE = 410, ///< HTTP 410 Gone. + AZ_HTTP_STATUS_CODE_LENGTH_REQUIRED = 411, ///< HTTP 411 Length Required. + AZ_HTTP_STATUS_CODE_PRECONDITION_FAILED = 412, ///< HTTP 412 Precondition Failed. + AZ_HTTP_STATUS_CODE_PAYLOAD_TOO_LARGE = 413, ///< HTTP 413 Payload Too Large. + AZ_HTTP_STATUS_CODE_URI_TOO_LONG = 414, ///< HTTP 414 URI Too Long. + AZ_HTTP_STATUS_CODE_UNSUPPORTED_MEDIA_TYPE = 415, ///< HTTP 415 Unsupported Media Type. + AZ_HTTP_STATUS_CODE_RANGE_NOT_SATISFIABLE = 416, ///< HTTP 416 Range Not Satisfiable. + AZ_HTTP_STATUS_CODE_EXPECTATION_FAILED = 417, ///< HTTP 417 Expectation Failed. + AZ_HTTP_STATUS_CODE_MISDIRECTED_REQUEST = 421, ///< HTTP 421 Misdirected Request. + AZ_HTTP_STATUS_CODE_UNPROCESSABLE_ENTITY = 422, ///< HTTP 422 Unprocessable Entity. + AZ_HTTP_STATUS_CODE_LOCKED = 423, ///< HTTP 423 Locked. + AZ_HTTP_STATUS_CODE_FAILED_DEPENDENCY = 424, ///< HTTP 424 Failed Dependency. + AZ_HTTP_STATUS_CODE_TOO_EARLY = 425, ///< HTTP 425 Too Early. + AZ_HTTP_STATUS_CODE_UPGRADE_REQUIRED = 426, ///< HTTP 426 Upgrade Required. + AZ_HTTP_STATUS_CODE_PRECONDITION_REQUIRED = 428, ///< HTTP 428 Precondition Required. + AZ_HTTP_STATUS_CODE_TOO_MANY_REQUESTS = 429, ///< HTTP 429 Too Many Requests. + AZ_HTTP_STATUS_CODE_REQUEST_HEADER_FIELDS_TOO_LARGE + = 431, ///< HTTP 431 Request Header Fields Too Large. + AZ_HTTP_STATUS_CODE_UNAVAILABLE_FOR_LEGAL_REASONS + = 451, ///< HTTP 451 Unavailable For Legal Reasons. + + // === 5xx (server error) Status Codes: === + AZ_HTTP_STATUS_CODE_INTERNAL_SERVER_ERROR = 500, ///< HTTP 500 Internal Server Error. + AZ_HTTP_STATUS_CODE_NOT_IMPLEMENTED = 501, ///< HTTP 501 Not Implemented. + AZ_HTTP_STATUS_CODE_BAD_GATEWAY = 502, ///< HTTP 502 Bad Gateway. + AZ_HTTP_STATUS_CODE_SERVICE_UNAVAILABLE = 503, ///< HTTP 503 Unavailable. + AZ_HTTP_STATUS_CODE_GATEWAY_TIMEOUT = 504, ///< HTTP 504 Gateway Timeout. + AZ_HTTP_STATUS_CODE_HTTP_VERSION_NOT_SUPPORTED = 505, ///< HTTP 505 HTTP Version Not Supported. + AZ_HTTP_STATUS_CODE_VARIANT_ALSO_NEGOTIATES = 506, ///< HTTP 506 Variant Also Negotiates. + AZ_HTTP_STATUS_CODE_INSUFFICIENT_STORAGE = 507, ///< HTTP 507 Insufficient Storage. + AZ_HTTP_STATUS_CODE_LOOP_DETECTED = 508, ///< HTTP 508 Loop Detected. + AZ_HTTP_STATUS_CODE_NOT_EXTENDED = 510, ///< HTTP 510 Not Extended. + AZ_HTTP_STATUS_CODE_NETWORK_AUTHENTICATION_REQUIRED + = 511, ///< HTTP 511 Network Authentication Required. +} az_http_status_code; + +/** + * @brief Allows you to customize the retry policy used by SDK clients whenever they perform an I/O + * operation. + * + * @details Client libraries should acquire an initialized instance of this struct and then modify + * any fields necessary before passing a pointer to this struct when initializing the specific + * client. + */ +typedef struct +{ + /// The minimum time, in milliseconds, to wait before a retry. + int32_t retry_delay_msec; + + /// The maximum time, in milliseconds, to wait before a retry. + int32_t max_retry_delay_msec; + + /// Maximum number of retries. + int32_t max_retries; +} az_http_policy_retry_options; + +typedef enum +{ + _az_HTTP_RESPONSE_KIND_STATUS_LINE = 0, + _az_HTTP_RESPONSE_KIND_HEADER = 1, + _az_HTTP_RESPONSE_KIND_BODY = 2, + _az_HTTP_RESPONSE_KIND_EOF = 3, +} _az_http_response_kind; + +/** + * @brief Allows you to parse an HTTP response's status line, headers, and body. + * + * @details Users create an instance of this and pass it in to an Azure service client's operation + * function. The function initializes the #az_http_response and application code can query the + * response after the operation completes by calling the #az_http_response_get_status_line(), + * #az_http_response_get_next_header() and #az_http_response_get_body() functions. + */ +typedef struct +{ + struct + { + az_span http_response; + int32_t written; + struct + { + az_span remaining; // the remaining un-parsed portion of the original http_response. + _az_http_response_kind next_kind; + // After parsing an element, next_kind refers to the next expected element + } parser; + } _internal; +} az_http_response; + +/** + * @brief Initializes an #az_http_response instance over a byte buffer (span) which will be filled + * with the HTTP response data as it comes in from the network. + * + * @param[out] out_response The pointer to an #az_http_response instance which is to be initialized. + * @param[in] buffer A span over the byte buffer that is to be filled with the HTTP response data. + * This buffer must be large enough to hold the entire response. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval other Initialization failed. + */ +AZ_NODISCARD AZ_INLINE az_result +az_http_response_init(az_http_response* out_response, az_span buffer) +{ + *out_response = (az_http_response){ + ._internal = { + .http_response = buffer, + .written = 0, + .parser = { + .remaining = AZ_SPAN_EMPTY, + .next_kind = _az_HTTP_RESPONSE_KIND_STATUS_LINE, + }, + }, + }; + + return AZ_OK; +} + +/** + * @brief Represents the result of making an HTTP request. + * An application obtains this initialized structure by calling #az_http_response_get_status_line(). + * + * @see https://tools.ietf.org/html/rfc7230#section-3.1.2 + */ +// Member order is optimized for alignment. +typedef struct +{ + az_span reason_phrase; ///< Reason Phrase. + az_http_status_code status_code; ///< Status Code. + uint8_t major_version; ///< HTTP Major Version. + uint8_t minor_version; ///< HTTP Minor Version. +} az_http_response_status_line; + +/** + * @brief Returns the #az_http_response_status_line information within an HTTP response. + * + * @param[in,out] ref_response The #az_http_response with an HTTP response. + * @param[out] out_status_line The pointer to an #az_http_response_status_line structure to be + * filled in by this function. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Response status line was parsed to \p out_status_line. + * @retval other HTTP response was not parsed. + */ +AZ_NODISCARD az_result az_http_response_get_status_line( + az_http_response* ref_response, + az_http_response_status_line* out_status_line); + +/** + * @brief Returns the #az_http_status_code information from an HTTP response. + * @details Invokes #az_http_response_get_status_line(), so it advances the reading position + * accordingly. + * + * @remark Use this function when HTTP Reason Phrase is not needed, and when it is not important for + * the invoking code to distinguish between #AZ_HTTP_STATUS_CODE_NONE and any possible error when + * parsing an HTTP response. + * + * @param[in,out] ref_response The #az_http_response with an HTTP response. + * + * @return An HTTP status code. + */ +AZ_INLINE AZ_NODISCARD az_http_status_code +az_http_response_get_status_code(az_http_response* ref_response) +{ + az_http_response_status_line status_line = { 0 }; + return az_result_failed(az_http_response_get_status_line(ref_response, &status_line)) + ? AZ_HTTP_STATUS_CODE_NONE + : status_line.status_code; +} + +/** + * @brief Returns the next HTTP response header. + * + * @details + * When called right after #az_http_response_get_status_line(), or after + * #az_http_response_get_status_code(), this function returns the first header. When called after + * calling #az_http_response_get_next_header(), this function returns the next header. + * + * If called after parsing HTTP body or before parsing status line, this function + * will return #AZ_ERROR_HTTP_INVALID_STATE. + * + * @param[in,out] ref_response A pointer to an #az_http_response instance. + * @param[out] out_name A pointer to an #az_span to receive the header's name. + * @param[out] out_value A pointer to an #az_span to receive the header's value. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK A header was returned. + * @retval #AZ_ERROR_HTTP_END_OF_HEADERS There are no more headers within the HTTP response payload. + * @retval #AZ_ERROR_HTTP_CORRUPT_RESPONSE_HEADER The HTTP response contains an unexpected invalid + * character or is incomplete. + * @retval #AZ_ERROR_HTTP_INVALID_STATE The #az_http_response instance is in an invalid state. + * Consider calling #az_http_response_get_status_line() to reset its state. + */ +AZ_NODISCARD az_result az_http_response_get_next_header( + az_http_response* ref_response, + az_span* out_name, + az_span* out_value); + +/** + * @brief Returns a span over the HTTP body within an HTTP response. + * + * @param[in,out] ref_response A pointer to an #az_http_response instance. + * @param[out] out_body A pointer to an #az_span to receive the HTTP response's body. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK An #az_span over the response body was returned. + * @retval other Error while trying to read and parse body. + */ +AZ_NODISCARD az_result az_http_response_get_body(az_http_response* ref_response, az_span* out_body); + +#include <_az_cfg_suffix.h> + +#endif // _az_HTTP_H diff --git a/src/az_http_header_validation_private.h b/src/az_http_header_validation_private.h new file mode 100644 index 00000000..e90fe9f3 --- /dev/null +++ b/src/az_http_header_validation_private.h @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file az_http_header_validation_private.h + * + * @brief This header defines a bit array that is used to validate whenever or not an ASCII char is + * valid within an http header name. + * + */ + +#ifndef _az_HTTP_HEADER_VALIDATION_PRIVATE_H +#define _az_HTTP_HEADER_VALIDATION_PRIVATE_H + +#include + +#include +#include + +#include <_az_cfg_prefix.h> + +// Bit array from ASCII to valid. Every zero in array means invalid character, while non-zeros +// return the valid character. +static const uint8_t az_http_valid_token[256] = { + 0, // 0 -> null + 0, // 1 -> start of heading + 0, // 2 -> start of text + 0, // 3 -> end of text + 0, // 4 -> end of transmission + 0, // 5 -> enquiry + 0, // 6 -> acknowledge + 0, // 7 -> bell + 0, // 8 -> backspace + 0, // 9 -> horizontal tab + 0, // 10 -> new line + 0, // 11 -> vertical tab + 0, // 12 -> new page + 0, // 13 -> carriage return + 0, // 14 -> shift out + 0, // 15 -> shift in + 0, // 16 -> data link escape + 0, // 17 -> device control 1 + 0, // 18 -> device control 2 + 0, // 19 -> device control 3 + 0, // 20 -> device control 4 + 0, // 21 -> negative acknowledge + 0, // 22 -> synchronous idle + 0, // 23 -> end of trans. block + 0, // 24 -> cancel + 0, // 25 -> end of medium + 0, // 26 -> substitute + 0, // 27 -> escape + 0, // 28 -> file separator + 0, // 29 -> group separator + 0, // 30 -> record separator + 0, // 31 -> unit separator + ' ', // 32 -> space + '!', // 33 -> ! + 0, // 34 -> " + '#', // 35 -> # + '$', // 36 -> $ + '%', // 37 -> % + '&', // 38 -> & + '\'', // 39 -> ' + 0, // 40 -> ( + 0, // 41 -> ) + '*', // 42 -> * + '+', // 43 -> + + 0, // 44 -> , + '-', // 45 -> - + '.', // 46 -> . + 0, // 47 -> / + '0', // 48 -> 0 + '1', // 49 -> 1 + '2', // 50 -> 2 + '3', // 51 -> 3 + '4', // 52 -> 4 + '5', // 53 -> 5 + '6', // 54 -> 6 + '7', // 55 -> 7 + '8', // 56 -> 8 + '9', // 57 -> 9 + 0, // 58 -> : + 0, // 59 -> ; + 0, // 60 -> < + 0, // 61 -> = + 0, // 62 -> > + 0, // 63 -> ? + 0, // 64 -> @ + 'a', // 65 -> A + 'b', // 66 -> B + 'c', // 67 -> C + 'd', // 68 -> D + 'e', // 69 -> E + 'f', // 70 -> F + 'g', // 71 -> G + 'h', // 72 -> H + 'i', // 73 -> I + 'j', // 74 -> J + 'k', // 75 -> K + 'l', // 76 -> L + 'm', // 77 -> M + 'n', // 78 -> N + 'o', // 79 -> O + 'p', // 80 -> P + 'q', // 81 -> Q + 'r', // 82 -> R + 's', // 83 -> S + 't', // 84 -> T + 'u', // 85 -> U + 'v', // 86 -> V + 'w', // 87 -> W + 'x', // 88 -> X + 'y', // 89 -> Y + 'z', // 90 -> Z + 0, // 91 -> [ + 0, // 92 -> comment + 0, // 93 -> ] + '^', // 94 -> ^ + '_', // 95 -> _ + '`', // 96 -> ` + 'a', // 97 -> a + 'b', // 98 -> b + 'c', // 99 -> c + 'd', // 100 -> d + 'e', // 101 -> e + 'f', // 102 -> f + 'g', // 103 -> g + 'h', // 104 -> h + 'i', // 105 -> i + 'j', // 106 -> j + 'k', // 107 -> k + 'l', // 108 -> l + 'm', // 109 -> m + 'n', // 110 -> n + 'o', // 111 -> o + 'p', // 112 -> p + 'q', // 113 -> q + 'r', // 114 -> r + 's', // 115 -> s + 't', // 116 -> t + 'u', // 117 -> u + 'v', // 118 -> v + 'w', // 119 -> w + 'x', // 120 -> x + 'y', // 121 -> y + 'z', // 122 -> z + 0, // 123 -> { + '|', // 124 -> | + 0, // 125 -> } + '~', // 126 -> ~ + 0 // 127 -> DEL + // ...128-255 is all zeros (not valid) characters +}; + +#ifndef AZ_NO_PRECONDITION_CHECKING + +/* This function is for httpRequest only to check header names are valid. + * Validation is a Precondition so this code would compile away if preconditions are OFF + */ +AZ_NODISCARD AZ_INLINE bool az_http_is_valid_header_name(az_span name) +{ + uint8_t* name_ptr = az_span_ptr(name); + for (int32_t i = 0; i < az_span_size(name); i++) + { + uint8_t c = name_ptr[i]; + if (az_http_valid_token[c] == 0) + { + return false; + } + } + return true; +} + +#endif // AZ_NO_PRECONDITION_CHECKING + +#include <_az_cfg_suffix.h> + +#endif // _az_HTTP_HEADER_VALIDATION_PRIVATE_H diff --git a/src/az_http_internal.h b/src/az_http_internal.h new file mode 100644 index 00000000..c219289e --- /dev/null +++ b/src/az_http_internal.h @@ -0,0 +1,255 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#ifndef _az_HTTP_INTERNAL_H +#define _az_HTTP_INTERNAL_H + +#include +#include +#include +#include +#include + +#include <_az_cfg_prefix.h> + +enum +{ + /// The maximum number of HTTP pipeline policies allowed. + _az_MAXIMUM_NUMBER_OF_POLICIES = 10, +}; + +/** + * @brief Internal definition of an HTTP pipeline. + * Defines the number of policies inside a pipeline. + */ +typedef struct +{ + struct + { + _az_http_policy policies[_az_MAXIMUM_NUMBER_OF_POLICIES]; + } _internal; +} _az_http_pipeline; + +typedef enum +{ + _az_http_policy_apiversion_option_location_header, + _az_http_policy_apiversion_option_location_queryparameter +} _az_http_policy_apiversion_option_location; + +/** + * @brief Defines the options structure used by the API Version policy. + */ +typedef struct +{ + // Services pass API versions in the header or in query parameters + struct + { + az_span name; + az_span version; + + // Avoid using enum as the first field within structs, to allow for { 0 } initialization. + // This is a workaround for IAR compiler warning [Pe188]: enumerated type mixed with another + // type. + + _az_http_policy_apiversion_option_location option_location; + } _internal; +} _az_http_policy_apiversion_options; + +/** + * @brief options for the telemetry policy + * os = string representation of currently executing Operating System + * + */ +typedef struct +{ + az_span component_name; +} _az_http_policy_telemetry_options; + +/** + * @brief Creates _az_http_policy_telemetry_options with default values. + * + * @param[in] component_name The name of the SDK component. + * + * @return Initialized telemetry options. + */ +AZ_NODISCARD AZ_INLINE _az_http_policy_telemetry_options +_az_http_policy_telemetry_options_create(az_span component_name) +{ + _az_PRECONDITION_VALID_SPAN(component_name, 1, false); + return (_az_http_policy_telemetry_options){ .component_name = component_name }; +} + +AZ_NODISCARD AZ_INLINE _az_http_policy_apiversion_options +_az_http_policy_apiversion_options_default() +{ + return (_az_http_policy_apiversion_options){ + ._internal = { .option_location = _az_http_policy_apiversion_option_location_header, + .name = AZ_SPAN_EMPTY, + .version = AZ_SPAN_EMPTY } + }; +} + +/** + * @brief Initialize az_http_policy_retry_options with default values + * + */ +AZ_NODISCARD az_http_policy_retry_options _az_http_policy_retry_options_default(); + +// PipelinePolicies +// Policies are non-allocating caveat the TransportPolicy +// Transport policies can only allocate if the transport layer they call allocates +// Client -> +// ===HttpPipelinePolicies=== +// UniqueRequestID +// Retry +// Authentication +// Logging +// Buffer Response +// Distributed Tracing +// TransportPolicy +// ===Transport Layer=== +// PipelinePolicies must implement the process function +// + +// Start the pipeline +AZ_NODISCARD az_result az_http_pipeline_process( + _az_http_pipeline* ref_pipeline, + az_http_request* ref_request, + az_http_response* ref_response); + +AZ_NODISCARD az_result az_http_pipeline_policy_apiversion( + _az_http_policy* ref_policies, + void* ref_options, + az_http_request* ref_request, + az_http_response* ref_response); + +AZ_NODISCARD az_result az_http_pipeline_policy_telemetry( + _az_http_policy* ref_policies, + void* ref_options, + az_http_request* ref_request, + az_http_response* ref_response); + +AZ_NODISCARD az_result az_http_pipeline_policy_retry( + _az_http_policy* ref_policies, + void* ref_options, + az_http_request* ref_request, + az_http_response* ref_response); + +AZ_NODISCARD az_result az_http_pipeline_policy_credential( + _az_http_policy* ref_policies, + void* ref_options, + az_http_request* ref_request, + az_http_response* ref_response); + +#ifndef AZ_NO_LOGGING +AZ_NODISCARD az_result az_http_pipeline_policy_logging( + _az_http_policy* ref_policies, + void* ref_options, + az_http_request* ref_request, + az_http_response* ref_response); +#endif // AZ_NO_LOGGING + +AZ_NODISCARD az_result az_http_pipeline_policy_transport( + _az_http_policy* ref_policies, + void* ref_options, + az_http_request* ref_request, + az_http_response* ref_response); + +AZ_NODISCARD AZ_INLINE az_result _az_http_pipeline_nextpolicy( + _az_http_policy* ref_policies, + az_http_request* ref_request, + az_http_response* ref_response) +{ + // Transport Policy is the last policy in the pipeline + // it returns without calling nextpolicy + if (ref_policies[0]._internal.process == NULL) + { + return AZ_ERROR_HTTP_PIPELINE_INVALID_POLICY; + } + + return ref_policies[0]._internal.process( + &(ref_policies[1]), ref_policies[0]._internal.options, ref_request, ref_response); +} + +/** + * @brief Format buffer as a http request containing URL and header spans. + * + * @remark The initial \p url provided by the caller is expected to already be url-encoded. + * + * @param[out] out_request HTTP request to initialize. + * @param[in] context A pointer to an #az_context node. + * @param[in] method HTTP verb: `"GET"`, `"POST"`, etc. + * @param[in] url The #az_span to be used for storing the url. An initial value is expected to be in + * the buffer containing url schema and the server address. It can contain query parameters (like + * https://service.azure.com?query=1). This value is expected to be url-encoded. + * @param[in] url_length The size of the initial url value within url #az_span. + * @param[in] headers_buffer The #az_span to be used for storing headers for the request. The total + * number of headers are calculated automatically based on the size of the buffer. + * @param[in] body The #az_span buffer that contains a payload for the request. Use #AZ_SPAN_EMPTY + * for requests that don't have a body. + * + * @return + * - *`AZ_OK`* success. + * - *`AZ_ERROR_ARG`* + * - `out_request` is _NULL_. + * - `url`, `method`, or `headers_buffer` are invalid spans (see @ref _az_span_is_valid). + */ +AZ_NODISCARD az_result az_http_request_init( + az_http_request* out_request, + az_context* context, + az_http_method method, + az_span url, + int32_t url_length, + az_span headers_buffer, + az_span body); + +/** + * @brief Set a query parameter at the end of url. + * + * @remark Query parameters are stored url-encoded. This function will not check if + * the a query parameter already exists in the URL. Calling this function twice with same \p name + * would duplicate the query parameter. + * + * @param[out] ref_request HTTP request that holds the URL to set the query parameter to. + * @param[in] name URL parameter name. + * @param[in] value URL parameter value. + * @param[in] \p is_value_url_encoded boolean value that defines if the query parameter (name and + * value) is url-encoded or not. + * + * @remarks if \p is_value_url_encoded is set to false, before setting query parameter, it would be + * url-encoded. + * + * @return + * - *`AZ_OK`* success. + * - *`AZ_ERROR_NOT_ENOUGH_SPACE`* the `URL` would grow past the `max_url_size`, should + * the parameter get set. + * - *`AZ_ERROR_ARG`* + * - `p_request` is _NULL_. + * - `name` or `value` are invalid spans (see @ref _az_span_is_valid). + * - `name` or `value` are empty. + * - `name`'s or `value`'s buffer overlap resulting `url`'s buffer. + */ +AZ_NODISCARD az_result az_http_request_set_query_parameter( + az_http_request* ref_request, + az_span name, + az_span value, + bool is_value_url_encoded); + +/** + * @brief Add a new HTTP header for the request. + * + * @param ref_request HTTP request builder that holds the URL to set the query parameter to. + * @param name Header name (e.g. `"Content-Type"`). + * @param value Header value (e.g. `"application/x-www-form-urlencoded"`). + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE There isn't enough space in the \p ref_request to add a + * header. + */ +AZ_NODISCARD az_result +az_http_request_append_header(az_http_request* ref_request, az_span name, az_span value); + +#include <_az_cfg_suffix.h> + +#endif // _az_HTTP_INTERNAL_H diff --git a/src/az_http_pipeline.c b/src/az_http_pipeline.c new file mode 100644 index 00000000..e582f097 --- /dev/null +++ b/src/az_http_pipeline.c @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include +#include +#include + +#include <_az_cfg.h> + +AZ_NODISCARD az_result az_http_pipeline_process( + _az_http_pipeline* ref_pipeline, + az_http_request* ref_request, + az_http_response* ref_response) +{ + _az_PRECONDITION_NOT_NULL(ref_request); + _az_PRECONDITION_NOT_NULL(ref_response); + _az_PRECONDITION_NOT_NULL(ref_pipeline); + + return ref_pipeline->_internal.policies[0]._internal.process( + &(ref_pipeline->_internal.policies[1]), + ref_pipeline->_internal.policies[0]._internal.options, + ref_request, + ref_response); +} diff --git a/src/az_http_policy.c b/src/az_http_policy.c new file mode 100644 index 00000000..08cf9f5e --- /dev/null +++ b/src/az_http_policy.c @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "az_http_private.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#include <_az_cfg.h> + +AZ_NODISCARD az_result az_http_pipeline_policy_apiversion( + _az_http_policy* ref_policies, + void* ref_options, + az_http_request* ref_request, + az_http_response* ref_response) +{ + + _az_http_policy_apiversion_options const* const options + = (_az_http_policy_apiversion_options const*)ref_options; + + switch (options->_internal.option_location) + { + case _az_http_policy_apiversion_option_location_header: + // Add the version as a header + _az_RETURN_IF_FAILED(az_http_request_append_header( + ref_request, options->_internal.name, options->_internal.version)); + break; + case _az_http_policy_apiversion_option_location_queryparameter: + // Add the version as a query parameter. This value doesn't need url-encoding. Use `true` for + // url-encode to avoid encoding. + _az_RETURN_IF_FAILED(az_http_request_set_query_parameter( + ref_request, options->_internal.name, options->_internal.version, true)); + break; + default: + return AZ_ERROR_ARG; + } + + return _az_http_pipeline_nextpolicy(ref_policies, ref_request, ref_response); +} + +// "-1" below is to account for the null terminator at the end of the string. +#define _az_TELEMETRY_ID_PREFIX "azsdk-c-" +#define _az_TELEMETRY_ID_PREFIX_LENGTH (sizeof(_az_TELEMETRY_ID_PREFIX) - 1) +#define _az_TELEMETRY_COMPONENT_NAME_MAX_LENGTH 40 +#define _az_TELEMETRY_VERSION_MAX_LENGTH (sizeof("12.345.6789-preview.123") - 1) +#define _az_TELEMETRY_ID_MAX_LENGTH \ + (_az_TELEMETRY_ID_PREFIX_LENGTH + _az_TELEMETRY_COMPONENT_NAME_MAX_LENGTH + sizeof('/') \ + + _az_TELEMETRY_VERSION_MAX_LENGTH) + +AZ_NODISCARD az_result az_http_pipeline_policy_telemetry( + _az_http_policy* ref_policies, + void* ref_options, + az_http_request* ref_request, + az_http_response* ref_response) +{ + _az_PRECONDITION_NOT_NULL(ref_options); + + // Format spec: https://azure.github.io/azure-sdk/general_azurecore.html#telemetry-policy + uint8_t telemetry_id_buffer[_az_TELEMETRY_ID_MAX_LENGTH] = _az_TELEMETRY_ID_PREFIX; + az_span telemetry_id = AZ_SPAN_FROM_BUFFER(telemetry_id_buffer); + { + az_span remainder = az_span_slice_to_end(telemetry_id, _az_TELEMETRY_ID_PREFIX_LENGTH); + + _az_http_policy_telemetry_options* options = (_az_http_policy_telemetry_options*)(ref_options); + az_span const component_name = options->component_name; +#ifndef AZ_NO_PRECONDITION_CHECKING + { + int32_t const component_name_size = az_span_size(component_name); + _az_PRECONDITION_RANGE(1, component_name_size, _az_TELEMETRY_COMPONENT_NAME_MAX_LENGTH); + } +#endif // AZ_NO_PRECONDITION_CHECKING + remainder = az_span_copy(remainder, component_name); + + remainder = az_span_copy_u8(remainder, '/'); + remainder = az_span_copy(remainder, AZ_SPAN_FROM_STR(AZ_SDK_VERSION_STRING)); + + telemetry_id = az_span_slice(telemetry_id, 0, _az_span_diff(remainder, telemetry_id)); + } + + _az_RETURN_IF_FAILED( + az_http_request_append_header(ref_request, AZ_SPAN_FROM_STR("User-Agent"), telemetry_id)); + + return _az_http_pipeline_nextpolicy(ref_policies, ref_request, ref_response); +} + +#undef _az_TELEMETRY_ID_PREFIX +#undef _az_TELEMETRY_ID_PREFIX_LENGTH +#undef _az_TELEMETRY_COMPONENT_NAME_MAX_LENGTH +#undef _az_TELEMETRY_VERSION_MAX_LENGTH +#undef _az_TELEMETRY_ID_MAX_LENGTH + +AZ_NODISCARD az_result az_http_pipeline_policy_credential( + _az_http_policy* ref_policies, + void* ref_options, + az_http_request* ref_request, + az_http_response* ref_response) +{ + _az_credential* const credential = (_az_credential*)ref_options; + _az_http_policy_process_fn const policy_credential_apply + = credential == NULL ? NULL : credential->_internal.apply_credential_policy; + + if (credential == AZ_CREDENTIAL_ANONYMOUS || policy_credential_apply == NULL) + { + return _az_http_pipeline_nextpolicy(ref_policies, ref_request, ref_response); + } + + return policy_credential_apply(ref_policies, credential, ref_request, ref_response); +} + +AZ_NODISCARD az_result az_http_pipeline_policy_transport( + _az_http_policy* ref_policies, + void* ref_options, + az_http_request* ref_request, + az_http_response* ref_response) +{ + (void)ref_policies; // this is the last policy in the pipeline, we just void it + (void)ref_options; + + // make sure the response is resetted + _az_http_response_reset(ref_response); + + return az_http_client_send_request(ref_request, ref_response); +} diff --git a/src/az_http_policy_logging.c b/src/az_http_policy_logging.c new file mode 100644 index 00000000..bd8443cc --- /dev/null +++ b/src/az_http_policy_logging.c @@ -0,0 +1,267 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "az_http_policy_logging_private.h" +#include "az_span_private.h" +#include +#include +#include +#include +#include +#include + +#include <_az_cfg.h> + +enum +{ + _az_LOG_LENGTHY_VALUE_MAX_LENGTH + = 50, // When we print values, such as header values, if they are longer than + // _az_LOG_VALUE_MAX_LENGTH, we trim their contents (decorate with ellipsis in the middle) + // to make sure each individual header value does not exceed _az_LOG_VALUE_MAX_LENGTH so + // that they don't blow up the logs. +}; + +static az_span _az_http_policy_logging_copy_lengthy_value(az_span ref_log_msg, az_span value) +{ + int32_t value_size = az_span_size(value); + + // The caller should validate that ref_log_msg is large enough to contain the value az_span + // This means, ref_log_msg must have available at least _az_LOG_LENGTHY_VALUE_MAX_LENGTH (i.e. 50) + // bytes or as much as the size of the value az_span, whichever is smaller. + _az_PRECONDITION( + az_span_size(ref_log_msg) >= _az_LOG_LENGTHY_VALUE_MAX_LENGTH + || az_span_size(ref_log_msg) >= value_size); + + if (value_size <= _az_LOG_LENGTHY_VALUE_MAX_LENGTH) + { + return az_span_copy(ref_log_msg, value); + } + + az_span const ellipsis = AZ_SPAN_FROM_STR(" ... "); + int32_t const ellipsis_len = az_span_size(ellipsis); + + int32_t const first + = (_az_LOG_LENGTHY_VALUE_MAX_LENGTH / 2) - ((ellipsis_len / 2) + (ellipsis_len % 2)); // 22 + + int32_t const last + = ((_az_LOG_LENGTHY_VALUE_MAX_LENGTH / 2) + (_az_LOG_LENGTHY_VALUE_MAX_LENGTH % 2)) // 23 + - (ellipsis_len / 2); + + _az_PRECONDITION((first + last + ellipsis_len) == _az_LOG_LENGTHY_VALUE_MAX_LENGTH); + + ref_log_msg = az_span_copy(ref_log_msg, az_span_slice(value, 0, first)); + ref_log_msg = az_span_copy(ref_log_msg, ellipsis); + return az_span_copy(ref_log_msg, az_span_slice(value, value_size - last, value_size)); +} + +static az_result _az_http_policy_logging_append_http_request_msg( + az_http_request const* request, + az_span* ref_log_msg) +{ + static az_span const auth_header_name = AZ_SPAN_LITERAL_FROM_STR("authorization"); + + az_span http_request_string = AZ_SPAN_FROM_STR("HTTP Request : "); + az_span null_string = AZ_SPAN_FROM_STR("NULL"); + + int32_t required_length = az_span_size(http_request_string); + if (request == NULL) + { + required_length += az_span_size(null_string); + } + else + { + required_length = az_span_size(request->_internal.method) + request->_internal.url_length + 1; + } + + _az_RETURN_IF_NOT_ENOUGH_SIZE(*ref_log_msg, required_length); + + az_span remainder = az_span_copy(*ref_log_msg, http_request_string); + + if (request == NULL) + { + remainder = az_span_copy(remainder, null_string); + *ref_log_msg = az_span_slice(*ref_log_msg, 0, _az_span_diff(remainder, *ref_log_msg)); + return AZ_OK; + } + + remainder = az_span_copy(remainder, request->_internal.method); + remainder = az_span_copy_u8(remainder, ' '); + remainder = az_span_copy( + remainder, az_span_slice(request->_internal.url, 0, request->_internal.url_length)); + + int32_t const headers_count = az_http_request_headers_count(request); + + az_span new_line_tab_string = AZ_SPAN_FROM_STR("\n\t"); + az_span colon_separator_string = AZ_SPAN_FROM_STR(" : "); + + for (int32_t index = 0; index < headers_count; ++index) + { + az_span header_name = { 0 }; + az_span header_value = { 0 }; + _az_RETURN_IF_FAILED(az_http_request_get_header(request, index, &header_name, &header_value)); + + required_length = az_span_size(new_line_tab_string) + az_span_size(header_name); + if (az_span_size(header_value) > 0) + { + required_length += _az_LOG_LENGTHY_VALUE_MAX_LENGTH + az_span_size(colon_separator_string); + } + + _az_RETURN_IF_NOT_ENOUGH_SIZE(remainder, required_length); + remainder = az_span_copy(remainder, new_line_tab_string); + remainder = az_span_copy(remainder, header_name); + + if (az_span_size(header_value) > 0 && !az_span_is_content_equal(header_name, auth_header_name)) + { + remainder = az_span_copy(remainder, colon_separator_string); + remainder = _az_http_policy_logging_copy_lengthy_value(remainder, header_value); + } + } + *ref_log_msg = az_span_slice(*ref_log_msg, 0, _az_span_diff(remainder, *ref_log_msg)); + + return AZ_OK; +} + +static az_result _az_http_policy_logging_append_http_response_msg( + az_http_response* ref_response, + int64_t duration_msec, + az_http_request const* request, + az_span* ref_log_msg) +{ + az_span http_response_string = AZ_SPAN_FROM_STR("HTTP Response ("); + _az_RETURN_IF_NOT_ENOUGH_SIZE(*ref_log_msg, az_span_size(http_response_string)); + az_span remainder = az_span_copy(*ref_log_msg, http_response_string); + + _az_RETURN_IF_FAILED(az_span_i64toa(remainder, duration_msec, &remainder)); + + az_span ms_string = AZ_SPAN_FROM_STR("ms)"); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remainder, az_span_size(ms_string)); + remainder = az_span_copy(remainder, ms_string); + + if (ref_response == NULL || az_span_size(ref_response->_internal.http_response) == 0) + { + az_span is_empty_string = AZ_SPAN_FROM_STR(" is empty"); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remainder, az_span_size(is_empty_string)); + remainder = az_span_copy(remainder, is_empty_string); + + *ref_log_msg = az_span_slice(*ref_log_msg, 0, _az_span_diff(remainder, *ref_log_msg)); + return AZ_OK; + } + + az_span colon_separator_string = AZ_SPAN_FROM_STR(" : "); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remainder, az_span_size(colon_separator_string)); + remainder = az_span_copy(remainder, colon_separator_string); + + az_http_response_status_line status_line = { 0 }; + _az_RETURN_IF_FAILED(az_http_response_get_status_line(ref_response, &status_line)); + _az_RETURN_IF_FAILED(az_span_u64toa(remainder, (uint64_t)status_line.status_code, &remainder)); + + _az_RETURN_IF_NOT_ENOUGH_SIZE(remainder, az_span_size(status_line.reason_phrase) + 1); + remainder = az_span_copy_u8(remainder, ' '); + remainder = az_span_copy(remainder, status_line.reason_phrase); + + az_span new_line_tab_string = AZ_SPAN_FROM_STR("\n\t"); + + az_result result = AZ_OK; + az_span header_name = { 0 }; + az_span header_value = { 0 }; + while (az_result_succeeded( + result = az_http_response_get_next_header(ref_response, &header_name, &header_value))) + { + int32_t required_length = az_span_size(new_line_tab_string) + az_span_size(header_name); + if (az_span_size(header_value) > 0) + { + required_length += _az_LOG_LENGTHY_VALUE_MAX_LENGTH + az_span_size(colon_separator_string); + } + + _az_RETURN_IF_NOT_ENOUGH_SIZE(remainder, required_length); + + remainder = az_span_copy(remainder, new_line_tab_string); + remainder = az_span_copy(remainder, header_name); + + if (az_span_size(header_value) > 0) + { + remainder = az_span_copy(remainder, colon_separator_string); + remainder = _az_http_policy_logging_copy_lengthy_value(remainder, header_value); + } + } + + // Response payload was invalid or corrupted in some way. + if (result != AZ_ERROR_HTTP_END_OF_HEADERS) + { + return result; + } + + az_span new_lines_string = AZ_SPAN_FROM_STR("\n\n"); + az_span arrow_separator_string = AZ_SPAN_FROM_STR(" -> "); + int32_t required_length = az_span_size(new_lines_string) + az_span_size(arrow_separator_string); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remainder, required_length); + + remainder = az_span_copy(remainder, new_lines_string); + remainder = az_span_copy(remainder, arrow_separator_string); + + az_span append_request = remainder; + _az_RETURN_IF_FAILED(_az_http_policy_logging_append_http_request_msg(request, &append_request)); + + *ref_log_msg = az_span_slice( + *ref_log_msg, 0, _az_span_diff(remainder, *ref_log_msg) + az_span_size(append_request)); + return AZ_OK; +} + +void _az_http_policy_logging_log_http_request(az_http_request const* request) +{ + uint8_t log_msg_buf[AZ_LOG_MESSAGE_BUFFER_SIZE] = { 0 }; + az_span log_msg = AZ_SPAN_FROM_BUFFER(log_msg_buf); + + (void)_az_http_policy_logging_append_http_request_msg(request, &log_msg); + + _az_LOG_WRITE(AZ_LOG_HTTP_REQUEST, log_msg); +} + +void _az_http_policy_logging_log_http_response( + az_http_response const* response, + int64_t duration_msec, + az_http_request const* request) +{ + uint8_t log_msg_buf[AZ_LOG_MESSAGE_BUFFER_SIZE] = { 0 }; + az_span log_msg = AZ_SPAN_FROM_BUFFER(log_msg_buf); + + az_http_response response_copy = *response; + + (void)_az_http_policy_logging_append_http_response_msg( + &response_copy, duration_msec, request, &log_msg); + + _az_LOG_WRITE(AZ_LOG_HTTP_RESPONSE, log_msg); +} + +#ifndef AZ_NO_LOGGING +AZ_NODISCARD az_result az_http_pipeline_policy_logging( + _az_http_policy* ref_policies, + void* ref_options, + az_http_request* ref_request, + az_http_response* ref_response) +{ + (void)ref_options; + + if (_az_LOG_SHOULD_WRITE(AZ_LOG_HTTP_REQUEST)) + { + _az_http_policy_logging_log_http_request(ref_request); + } + + if (!_az_LOG_SHOULD_WRITE(AZ_LOG_HTTP_RESPONSE)) + { + // If no logging is needed, do not even measure the response time. + return _az_http_pipeline_nextpolicy(ref_policies, ref_request, ref_response); + } + + int64_t start = 0; + _az_RETURN_IF_FAILED(az_platform_clock_msec(&start)); + + az_result const result = _az_http_pipeline_nextpolicy(ref_policies, ref_request, ref_response); + + int64_t end = 0; + _az_RETURN_IF_FAILED(az_platform_clock_msec(&end)); + _az_http_policy_logging_log_http_response(ref_response, end - start, ref_request); + + return result; +} +#endif // AZ_NO_LOGGING diff --git a/src/az_http_policy_logging_private.h b/src/az_http_policy_logging_private.h new file mode 100644 index 00000000..751226c5 --- /dev/null +++ b/src/az_http_policy_logging_private.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#ifndef _az_HTTP_POLICY_LOGGING_PRIVATE_H +#define _az_HTTP_POLICY_LOGGING_PRIVATE_H + +#include +#include + +#include + +#include <_az_cfg_prefix.h> + +void _az_http_policy_logging_log_http_request(az_http_request const* request); + +void _az_http_policy_logging_log_http_response( + az_http_response const* response, + int64_t duration_msec, + az_http_request const* request); + +#include <_az_cfg_suffix.h> + +#endif // _az_HTTP_POLICY_LOGGING_PRIVATE_H diff --git a/src/az_http_policy_retry.c b/src/az_http_policy_retry.c new file mode 100644 index 00000000..3523294a --- /dev/null +++ b/src/az_http_policy_retry.c @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "az_http_private.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include <_az_cfg.h> + +AZ_NODISCARD az_http_policy_retry_options _az_http_policy_retry_options_default() +{ + return (az_http_policy_retry_options){ + .max_retries = 4, + .retry_delay_msec = 4 * _az_TIME_MILLISECONDS_PER_SECOND, // 4 seconds + .max_retry_delay_msec + = 2 * _az_TIME_SECONDS_PER_MINUTE * _az_TIME_MILLISECONDS_PER_SECOND, // 2 minutes + }; +} + +// TODO: Add unit tests +AZ_INLINE az_result _az_http_policy_retry_append_http_retry_msg( + int32_t attempt, + int32_t delay_msec, + az_span* ref_log_msg) +{ + az_span retry_count_string = AZ_SPAN_FROM_STR("HTTP Retry attempt #"); + _az_RETURN_IF_NOT_ENOUGH_SIZE(*ref_log_msg, az_span_size(retry_count_string)); + az_span remainder = az_span_copy(*ref_log_msg, retry_count_string); + + _az_RETURN_IF_FAILED(az_span_i32toa(remainder, attempt, &remainder)); + + az_span infix_string = AZ_SPAN_FROM_STR(" will be made in "); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remainder, az_span_size(infix_string)); + remainder = az_span_copy(remainder, infix_string); + + _az_RETURN_IF_FAILED(az_span_i32toa(remainder, delay_msec, &remainder)); + + az_span suffix_string = AZ_SPAN_FROM_STR("ms."); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remainder, az_span_size(suffix_string)); + remainder = az_span_copy(remainder, suffix_string); + + *ref_log_msg = az_span_slice(*ref_log_msg, 0, _az_span_diff(remainder, *ref_log_msg)); + + return AZ_OK; +} + +AZ_INLINE void _az_http_policy_retry_log(int32_t attempt, int32_t delay_msec) +{ + uint8_t log_msg_buf[AZ_LOG_MESSAGE_BUFFER_SIZE] = { 0 }; + az_span log_msg = AZ_SPAN_FROM_BUFFER(log_msg_buf); + + (void)_az_http_policy_retry_append_http_retry_msg(attempt, delay_msec, &log_msg); + + _az_LOG_WRITE(AZ_LOG_HTTP_RETRY, log_msg); +} + +AZ_INLINE AZ_NODISCARD int32_t _az_uint32_span_to_int32(az_span span) +{ + uint32_t value = 0; + if (az_result_failed(az_span_atou32(span, &value))) + { + return -1; + } + + return value < INT32_MAX ? (int32_t)value : INT32_MAX; +} + +AZ_INLINE AZ_NODISCARD bool _az_http_policy_retry_should_retry_http_response_code( + az_http_status_code http_response_code) +{ + switch (http_response_code) + { + case AZ_HTTP_STATUS_CODE_REQUEST_TIMEOUT: + case AZ_HTTP_STATUS_CODE_TOO_MANY_REQUESTS: + case AZ_HTTP_STATUS_CODE_INTERNAL_SERVER_ERROR: + case AZ_HTTP_STATUS_CODE_BAD_GATEWAY: + case AZ_HTTP_STATUS_CODE_SERVICE_UNAVAILABLE: + case AZ_HTTP_STATUS_CODE_GATEWAY_TIMEOUT: + return true; + default: + return false; + } +} + +AZ_INLINE AZ_NODISCARD az_result _az_http_policy_retry_get_retry_after( + az_http_response* ref_response, + bool* should_retry, + int32_t* retry_after_msec) +{ + az_http_response_status_line status_line = { 0 }; + _az_RETURN_IF_FAILED(az_http_response_get_status_line(ref_response, &status_line)); + + if (!_az_http_policy_retry_should_retry_http_response_code(status_line.status_code)) + { + *should_retry = false; + *retry_after_msec = -1; + return AZ_OK; + } + + *should_retry = true; + + // Try to get the value of retry-after header, if there's one. + az_span header_name = { 0 }; + az_span header_value = { 0 }; + while (az_result_succeeded( + az_http_response_get_next_header(ref_response, &header_name, &header_value))) + { + if (az_span_is_content_equal_ignoring_case(header_name, AZ_SPAN_FROM_STR("retry-after-ms")) + || az_span_is_content_equal_ignoring_case( + header_name, AZ_SPAN_FROM_STR("x-ms-retry-after-ms"))) + { + // The value is in milliseconds. + int32_t const msec = _az_uint32_span_to_int32(header_value); + if (msec >= 0) // int32_t max == ~24 days + { + *retry_after_msec = msec; + return AZ_OK; + } + } + else if (az_span_is_content_equal_ignoring_case(header_name, AZ_SPAN_FROM_STR("Retry-After"))) + { + // The value is either seconds or date. + int32_t const seconds = _az_uint32_span_to_int32(header_value); + if (seconds >= 0) // int32_t max == ~68 years + { + *retry_after_msec = (seconds <= (INT32_MAX / _az_TIME_MILLISECONDS_PER_SECOND)) + ? seconds * _az_TIME_MILLISECONDS_PER_SECOND + : INT32_MAX; + + return AZ_OK; + } + + // TODO: Other possible value is HTTP Date. For that, we'll need to parse date, get + // current date, subtract one from another, get seconds. And the device should have a + // sense of calendar clock. + } + } + + *retry_after_msec = -1; + return AZ_OK; +} + +AZ_NODISCARD az_result az_http_pipeline_policy_retry( + _az_http_policy* ref_policies, + void* ref_options, + az_http_request* ref_request, + az_http_response* ref_response) +{ + az_http_policy_retry_options const* const retry_options + = (az_http_policy_retry_options const*)ref_options; + + int32_t const max_retries = retry_options->max_retries; + int32_t const retry_delay_msec = retry_options->retry_delay_msec; + int32_t const max_retry_delay_msec = retry_options->max_retry_delay_msec; + + _az_RETURN_IF_FAILED(_az_http_request_mark_retry_headers_start(ref_request)); + + az_context* const context = ref_request->_internal.context; + + bool const should_log = _az_LOG_SHOULD_WRITE(AZ_LOG_HTTP_RETRY); + az_result result = AZ_OK; + int32_t attempt = 1; + while (true) + { + _az_RETURN_IF_FAILED( + az_http_response_init(ref_response, ref_response->_internal.http_response)); + _az_RETURN_IF_FAILED(_az_http_request_remove_retry_headers(ref_request)); + + result = _az_http_pipeline_nextpolicy(ref_policies, ref_request, ref_response); + + // Even HTTP 429, or 502 are expected to be AZ_OK, so the failed result is not retriable. + if (attempt > max_retries || az_result_failed(result)) + { + return result; + } + + int32_t retry_after_msec = -1; + bool should_retry = false; + az_http_response response_copy = *ref_response; + + _az_RETURN_IF_FAILED( + _az_http_policy_retry_get_retry_after(&response_copy, &should_retry, &retry_after_msec)); + + if (!should_retry) + { + return result; + } + + ++attempt; + + if (retry_after_msec < 0) + { // there wasn't any kind of "retry-after" response header + retry_after_msec = _az_retry_calc_delay(attempt, retry_delay_msec, max_retry_delay_msec); + } + + if (should_log) + { + _az_http_policy_retry_log(attempt, retry_after_msec); + } + + _az_RETURN_IF_FAILED(az_platform_sleep_msec(retry_after_msec)); + + if (context != NULL) + { + int64_t clock = 0; + _az_RETURN_IF_FAILED(az_platform_clock_msec(&clock)); + if (az_context_has_expired(context, clock)) + { + return AZ_ERROR_CANCELED; + } + } + } + + return result; +} diff --git a/src/az_http_private.h b/src/az_http_private.h new file mode 100644 index 00000000..24899e9b --- /dev/null +++ b/src/az_http_private.h @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#ifndef _az_HTTP_PRIVATE_H +#define _az_HTTP_PRIVATE_H + +#include +#include +#include +#include +#include + +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief Mark that the HTTP headers that are gong to be added via + * `az_http_request_append_header` are going to be considered as retry headers. + * + * @param ref_request HTTP request. + * + * @return + * - *`AZ_OK`* success. + * - *`AZ_ERROR_ARG`* `ref_request` is _NULL_. + */ +AZ_NODISCARD AZ_INLINE az_result +_az_http_request_mark_retry_headers_start(az_http_request* ref_request) +{ + _az_PRECONDITION_NOT_NULL(ref_request); + ref_request->_internal.retry_headers_start_byte_offset + = ref_request->_internal.headers_length * (int32_t)sizeof(_az_http_request_header); + return AZ_OK; +} + +AZ_NODISCARD AZ_INLINE az_result _az_http_request_remove_retry_headers(az_http_request* ref_request) +{ + _az_PRECONDITION_NOT_NULL(ref_request); + ref_request->_internal.headers_length = ref_request->_internal.retry_headers_start_byte_offset + / (int32_t)sizeof(_az_http_request_header); + return AZ_OK; +} + +/** + * @brief Sets buffer and parser to its initial state. + * + */ +void _az_http_response_reset(az_http_response* ref_response); + +#include <_az_cfg_suffix.h> + +#endif // _az_HTTP_PRIVATE_H diff --git a/src/az_http_request.c b/src/az_http_request.c new file mode 100644 index 00000000..5bccae48 --- /dev/null +++ b/src/az_http_request.c @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "az_http_header_validation_private.h" +#include "az_http_private.h" +#include "az_span_private.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include <_az_cfg.h> + +AZ_NODISCARD az_result az_http_request_init( + az_http_request* out_request, + az_context* context, + az_span method, + az_span url, + int32_t url_length, + az_span headers_buffer, + az_span body) +{ + _az_PRECONDITION_NOT_NULL(out_request); + _az_PRECONDITION_VALID_SPAN(method, 1, false); + _az_PRECONDITION_VALID_SPAN(url, 1, false); + _az_PRECONDITION_VALID_SPAN(headers_buffer, 0, false); + + int32_t query_start = 0; + uint8_t const* const ptr = az_span_ptr(url); + for (; query_start < url_length; ++query_start) + { + uint8_t next_byte = ptr[query_start]; + if (next_byte == '?') + { + break; + } + } + + *out_request + = (az_http_request){ ._internal = { + .context = context, + .method = method, + .url = url, + .url_length = url_length, + /* query start is set to 0 if there is not a question mark so the + next time query parameter is appended, a question mark will be + added at url length. (+1 jumps the `?`) */ + .query_start = query_start == url_length ? 0 : query_start + 1, + .headers = headers_buffer, + .headers_length = 0, + .max_headers = az_span_size(headers_buffer) + / (int32_t)sizeof(_az_http_request_header), + .retry_headers_start_byte_offset = 0, + .body = body, + } }; + + return AZ_OK; +} + +AZ_NODISCARD az_result az_http_request_set_query_parameter( + az_http_request* ref_request, + az_span name, + az_span value, + bool is_value_url_encoded) +{ + _az_PRECONDITION_NOT_NULL(ref_request); + _az_PRECONDITION_VALID_SPAN(name, 1, false); + _az_PRECONDITION_VALID_SPAN(value, 1, false); + + // name or value can't be empty + _az_PRECONDITION(az_span_size(name) > 0 && az_span_size(value) > 0); + + int32_t const initial_url_length = ref_request->_internal.url_length; + az_span url_remainder = az_span_slice_to_end(ref_request->_internal.url, initial_url_length); + + // Adding query parameter. Adding +2 to required length to include extra required symbols `=` + // and `?` or `&`. + int32_t required_length = 2 + az_span_size(name) + + (is_value_url_encoded ? az_span_size(value) : _az_span_url_encode_calc_length(value)); + + _az_RETURN_IF_NOT_ENOUGH_SIZE(url_remainder, required_length); + + // Append either '?' or '&' + uint8_t separator = '&'; + if (ref_request->_internal.query_start == 0) + { + separator = '?'; + + // update QPs starting position when it's 0 + ref_request->_internal.query_start = initial_url_length + 1; + } + + url_remainder = az_span_copy_u8(url_remainder, separator); + url_remainder = az_span_copy(url_remainder, name); + + // Append equal sym + url_remainder = az_span_copy_u8(url_remainder, '='); + + // Parameter value + if (is_value_url_encoded) + { + az_span_copy(url_remainder, value); + } + else + { + int32_t encoding_size = 0; + _az_RETURN_IF_FAILED(_az_span_url_encode(url_remainder, value, &encoding_size)); + } + + ref_request->_internal.url_length += required_length; + + return AZ_OK; +} + +AZ_NODISCARD az_result +az_http_request_append_header(az_http_request* ref_request, az_span name, az_span value) +{ + _az_PRECONDITION_NOT_NULL(ref_request); + + // remove whitespace characters from key and value + name = _az_span_trim_whitespace(name); + value = _az_span_trim_whitespace(value); + + _az_PRECONDITION_VALID_SPAN(name, 1, false); + + // Make this function to only work with valid input for header name + _az_PRECONDITION(az_http_is_valid_header_name(name)); + + az_span headers = ref_request->_internal.headers; + _az_http_request_header header_to_append = { .name = name, .value = value }; + + _az_RETURN_IF_NOT_ENOUGH_SIZE(headers, (int32_t)sizeof header_to_append); + + az_span_copy( + az_span_slice_to_end( + headers, + (int32_t)sizeof(_az_http_request_header) * ref_request->_internal.headers_length), + az_span_create((uint8_t*)&header_to_append, sizeof header_to_append)); + + ref_request->_internal.headers_length++; + + return AZ_OK; +} + +AZ_NODISCARD az_result az_http_request_get_header( + az_http_request const* request, + int32_t index, + az_span* out_name, + az_span* out_value) +{ + _az_PRECONDITION_NOT_NULL(request); + _az_PRECONDITION_NOT_NULL(out_name); + _az_PRECONDITION_NOT_NULL(out_value); + + if (index >= az_http_request_headers_count(request)) + { + return AZ_ERROR_ARG; + } + + _az_http_request_header const* const header + = &((_az_http_request_header*)az_span_ptr(request->_internal.headers))[index]; + + *out_name = header->name; + *out_value = header->value; + + return AZ_OK; +} + +AZ_NODISCARD az_result +az_http_request_get_method(az_http_request const* request, az_http_method* out_method) +{ + _az_PRECONDITION_NOT_NULL(request); + _az_PRECONDITION_NOT_NULL(out_method); + + *out_method = request->_internal.method; + + return AZ_OK; +} + +AZ_NODISCARD az_result az_http_request_get_url(az_http_request const* request, az_span* out_url) +{ + _az_PRECONDITION_NOT_NULL(request); + _az_PRECONDITION_NOT_NULL(out_url); + + *out_url = az_span_slice(request->_internal.url, 0, request->_internal.url_length); + + return AZ_OK; +} + +AZ_NODISCARD az_result az_http_request_get_body(az_http_request const* request, az_span* out_body) +{ + _az_PRECONDITION_NOT_NULL(request); + _az_PRECONDITION_NOT_NULL(out_body); + + *out_body = request->_internal.body; + return AZ_OK; +} + +AZ_NODISCARD int32_t az_http_request_headers_count(az_http_request const* request) +{ + return request->_internal.headers_length; +} diff --git a/src/az_http_response.c b/src/az_http_response.c new file mode 100644 index 00000000..fb487907 --- /dev/null +++ b/src/az_http_response.c @@ -0,0 +1,347 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include + +#include "az_http_header_validation_private.h" +#include "az_http_private.h" +#include "az_span_private.h" +#include +#include +#include + +#include <_az_cfg.h> +#include + +// HTTP Response utility functions + +static AZ_NODISCARD bool _az_is_http_whitespace(uint8_t c) +{ + switch (c) + { + case ' ': + case '\t': + return true; + ; + default: + return false; + } +} + +/* PRIVATE Function. parse next */ +static AZ_NODISCARD az_result _az_get_digit(az_span* ref_span, uint8_t* save_here) +{ + + if (az_span_size(*ref_span) == 0) + { + return AZ_ERROR_HTTP_CORRUPT_RESPONSE_HEADER; + } + + uint8_t c_ptr = az_span_ptr(*ref_span)[0]; + if (!isdigit(c_ptr)) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + // + *save_here = (uint8_t)(c_ptr - '0'); + + // move reader after the expected digit (means it was parsed as expected) + *ref_span = az_span_slice_to_end(*ref_span, 1); + + return AZ_OK; +} + +/** + * Status line https://tools.ietf.org/html/rfc7230#section-3.1.2 + * HTTP-version SP status-code SP reason-phrase CRLF + */ +static AZ_NODISCARD az_result +_az_get_http_status_line(az_span* ref_span, az_http_response_status_line* out_status_line) +{ + + // HTTP-version = HTTP-name "/" DIGIT "." DIGIT + // https://tools.ietf.org/html/rfc7230#section-2.6 + az_span const start = AZ_SPAN_FROM_STR("HTTP/"); + az_span const dot = AZ_SPAN_FROM_STR("."); + az_span const space = AZ_SPAN_FROM_STR(" "); + + // parse and move reader if success + _az_RETURN_IF_FAILED(_az_is_expected_span(ref_span, start)); + _az_RETURN_IF_FAILED(_az_get_digit(ref_span, &out_status_line->major_version)); + _az_RETURN_IF_FAILED(_az_is_expected_span(ref_span, dot)); + _az_RETURN_IF_FAILED(_az_get_digit(ref_span, &out_status_line->minor_version)); + + // SP = " " + _az_RETURN_IF_FAILED(_az_is_expected_span(ref_span, space)); + + // status-code = 3DIGIT + { + uint64_t code = 0; + _az_RETURN_IF_FAILED(az_span_atou64(az_span_create(az_span_ptr(*ref_span), 3), &code)); + out_status_line->status_code = (az_http_status_code)code; + // move reader + *ref_span = az_span_slice_to_end(*ref_span, 3); + } + + // SP + _az_RETURN_IF_FAILED(_az_is_expected_span(ref_span, space)); + + // get a pointer to read response until end of reason-phrase is found + // reason-phrase = *(HTAB / SP / VCHAR / obs-text) + // HTAB = "\t" + // VCHAR or obs-text is %x21-FF, + int32_t offset = 0; + int32_t input_size = az_span_size(*ref_span); + uint8_t const* const ptr = az_span_ptr(*ref_span); + for (; offset < input_size; ++offset) + { + uint8_t next_byte = ptr[offset]; + if (next_byte == '\n') + { + break; + } + } + if (offset == input_size) + { + return AZ_ERROR_ITEM_NOT_FOUND; + } + + // save reason-phrase in status line now that we got the offset. Remove 1 last chars(\r) + out_status_line->reason_phrase = az_span_slice(*ref_span, 0, offset - 1); + // move position of reader after reason-phrase (parsed done) + *ref_span = az_span_slice_to_end(*ref_span, offset + 1); + // CR LF + // _az_RETURN_IF_FAILED(_az_is_expected_span(response, AZ_SPAN_FROM_STR("\r\n"))); + + return AZ_OK; +} + +AZ_NODISCARD az_result az_http_response_get_status_line( + az_http_response* ref_response, + az_http_response_status_line* out_status_line) +{ + _az_PRECONDITION_NOT_NULL(ref_response); + _az_PRECONDITION_NOT_NULL(out_status_line); + + // Restart parser to the beginning + ref_response->_internal.parser.remaining = ref_response->_internal.http_response; + + // read an HTTP status line. + _az_RETURN_IF_FAILED( + _az_get_http_status_line(&ref_response->_internal.parser.remaining, out_status_line)); + + // set state.kind of the next HTTP response value. + ref_response->_internal.parser.next_kind = _az_HTTP_RESPONSE_KIND_HEADER; + + return AZ_OK; +} + +AZ_NODISCARD az_result az_http_response_get_next_header( + az_http_response* ref_response, + az_span* out_name, + az_span* out_value) +{ + _az_PRECONDITION_NOT_NULL(ref_response); + _az_PRECONDITION_NOT_NULL(out_name); + _az_PRECONDITION_NOT_NULL(out_value); + + az_span* reader = &ref_response->_internal.parser.remaining; + { + _az_http_response_kind const kind = ref_response->_internal.parser.next_kind; + // if reader is expecting to read body (all headers were read), return + // AZ_ERROR_HTTP_END_OF_HEADERS so we know we reach end of headers + if (kind == _az_HTTP_RESPONSE_KIND_BODY) + { + return AZ_ERROR_HTTP_END_OF_HEADERS; + } + // Can't read a header if status line was not previously called, + // User needs to call az_http_response_status_line() which would reset parser and set kind to + // headers + if (kind != _az_HTTP_RESPONSE_KIND_HEADER) + { + return AZ_ERROR_HTTP_INVALID_STATE; + } + } + + if (az_span_size(ref_response->_internal.parser.remaining) == 0) + { + // avoid reading address if span is size 0 + return AZ_ERROR_HTTP_CORRUPT_RESPONSE_HEADER; + } + + // check if we are at the end of all headers to change state to Body. + // We keep state to Headers if current char is not '\r' (there is another header) + if (az_span_ptr(ref_response->_internal.parser.remaining)[0] == '\r') + { + _az_RETURN_IF_FAILED(_az_is_expected_span(reader, AZ_SPAN_FROM_STR("\r\n"))); + ref_response->_internal.parser.next_kind = _az_HTTP_RESPONSE_KIND_BODY; + return AZ_ERROR_HTTP_END_OF_HEADERS; + } + + // https://tools.ietf.org/html/rfc7230#section-3.2 + // header-field = field-name ":" OWS field-value OWS + // field-name = token + { + // https://tools.ietf.org/html/rfc7230#section-3.2.6 + // token = 1*tchar + // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / + // "_" / "`" / "|" / "~" / DIGIT / ALPHA; + // any VCHAR, + // except delimiters + int32_t field_name_length = 0; + int32_t input_size = az_span_size(*reader); + uint8_t const* const ptr = az_span_ptr(*reader); + for (; field_name_length < input_size; ++field_name_length) + { + uint8_t next_byte = ptr[field_name_length]; + if (next_byte == ':') + { + break; + } + if (!az_http_valid_token[next_byte]) + { + return AZ_ERROR_HTTP_CORRUPT_RESPONSE_HEADER; + } + } + if (field_name_length == input_size) + { + return AZ_ERROR_HTTP_CORRUPT_RESPONSE_HEADER; + } + + // form a header name. Reader is currently at char ':' + *out_name = az_span_slice(*reader, 0, field_name_length); + // update reader to next position after colon (add one) + *reader = az_span_slice_to_end(*reader, field_name_length + 1); + + // Remove whitespace characters from header name + // https://github.com/Azure/azure-sdk-for-c/issues/604 + *out_name = _az_span_trim_whitespace(*out_name); + + // OWS -> remove the optional whitespace characters before header value + int32_t ows_len = 0; + input_size = az_span_size(*reader); + uint8_t const* const ptr_space = az_span_ptr(*reader); + for (; ows_len < input_size; ++ows_len) + { + uint8_t next_byte = ptr_space[ows_len]; + if (next_byte != ' ' && next_byte != '\t') + { + break; + } + } + if (ows_len == input_size) + { + return AZ_ERROR_HTTP_CORRUPT_RESPONSE_HEADER; + } + + *reader = az_span_slice_to_end(*reader, ows_len); + } + // field-value = *( field-content / obs-fold ) + // field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + // field-vchar = VCHAR / obs-text + // + // obs-fold = CRLF 1*( SP / HTAB ) + // ; obsolete line folding + // ; see Section 3.2.4 + // + // Note: obs-fold is not implemented. + { + int32_t offset = 0; + int32_t offset_value_end = offset; + while (true) + { + uint8_t c = az_span_ptr(*reader)[offset]; + offset += 1; + if (c == '\r') + { + break; // break as soon as end of value char is found + } + if (_az_is_http_whitespace(c)) + { + continue; // whitespace or tab is accepted. It can be any number after value (OWS) + } + if (c < ' ') + { + return AZ_ERROR_HTTP_CORRUPT_RESPONSE_HEADER; + } + offset_value_end = offset; // increasing index only for valid chars, + } + *out_value = az_span_slice(*reader, 0, offset_value_end); + // moving reader. It is currently after \r was found + *reader = az_span_slice_to_end(*reader, offset); + + // Remove whitespace characters from value https://github.com/Azure/azure-sdk-for-c/issues/604 + *out_value = _az_span_trim_whitespace_from_end(*out_value); + } + + _az_RETURN_IF_FAILED(_az_is_expected_span(reader, AZ_SPAN_FROM_STR("\n"))); + + return AZ_OK; +} + +AZ_NODISCARD az_result az_http_response_get_body(az_http_response* ref_response, az_span* out_body) +{ + _az_PRECONDITION_NOT_NULL(ref_response); + _az_PRECONDITION_NOT_NULL(out_body); + + // Make sure get body works no matter where is the current parsing. Allow users to call get body + // directly and ignore headers and status line + _az_http_response_kind current_parsing_section = ref_response->_internal.parser.next_kind; + if (current_parsing_section != _az_HTTP_RESPONSE_KIND_BODY) + { + if (current_parsing_section == _az_HTTP_RESPONSE_KIND_EOF + || current_parsing_section == _az_HTTP_RESPONSE_KIND_STATUS_LINE) + { + // Reset parser and get status line + az_http_response_status_line ignore = { 0 }; + _az_RETURN_IF_FAILED(az_http_response_get_status_line(ref_response, &ignore)); + // update current parsing section + current_parsing_section = ref_response->_internal.parser.next_kind; + } + // parse any remaining header + if (current_parsing_section == _az_HTTP_RESPONSE_KIND_HEADER) + { + // Parse and ignore all remaining headers + for (az_span n = { 0 }, v = { 0 }; + az_result_succeeded(az_http_response_get_next_header(ref_response, &n, &v));) + { + // ignoring header + } + } + } + + // take all the remaining content from reader as body + *out_body = az_span_slice_to_end(ref_response->_internal.parser.remaining, 0); + + ref_response->_internal.parser.next_kind = _az_HTTP_RESPONSE_KIND_EOF; + return AZ_OK; +} + +void _az_http_response_reset(az_http_response* ref_response) +{ + // never fails, discard the result + // init will set written to 0 and will use the same az_span. Internal parser's state is also + // reset + az_result result = az_http_response_init(ref_response, ref_response->_internal.http_response); + (void)result; +} + +// internal function to get az_http_response remainder +static az_span _az_http_response_get_remaining(az_http_response const* response) +{ + return az_span_slice_to_end(response->_internal.http_response, response->_internal.written); +} + +AZ_NODISCARD az_result az_http_response_append(az_http_response* ref_response, az_span source) +{ + _az_PRECONDITION_NOT_NULL(ref_response); + + az_span remaining = _az_http_response_get_remaining(ref_response); + int32_t write_size = az_span_size(source); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remaining, write_size); + + az_span_copy(remaining, source); + ref_response->_internal.written += write_size; + + return AZ_OK; +} diff --git a/src/az_http_transport.h b/src/az_http_transport.h new file mode 100644 index 00000000..8409cd7e --- /dev/null +++ b/src/az_http_transport.h @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Utilities to be used by HTTP transport policy implementations. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_HTTP_TRANSPORT_H +#define _az_HTTP_TRANSPORT_H + +#include +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief A type representing an HTTP method (`POST`, `PUT`, `GET`, `DELETE`, etc.). + */ +typedef az_span az_http_method; + +/** + * @brief HTTP GET method name. + */ +AZ_INLINE az_http_method az_http_method_get() { return AZ_SPAN_FROM_STR("GET"); } + +/** + * @brief HTTP HEAD method name. + */ +AZ_INLINE az_http_method az_http_method_head() { return AZ_SPAN_FROM_STR("HEAD"); } + +/** + * @brief HTTP POST method name. + */ +AZ_INLINE az_http_method az_http_method_post() { return AZ_SPAN_FROM_STR("POST"); } + +/** + * @brief HTTP PUT method name. + */ +AZ_INLINE az_http_method az_http_method_put() { return AZ_SPAN_FROM_STR("PUT"); } + +/** + * @brief HTTP DELETE method name. + */ +AZ_INLINE az_http_method az_http_method_delete() { return AZ_SPAN_FROM_STR("DELETE"); } + +/** + * @brief HTTP PATCH method name. + */ +AZ_INLINE az_http_method az_http_method_patch() { return AZ_SPAN_FROM_STR("PATCH"); } + +/** + * @brief Represents a name/value pair of #az_span instances. + */ +typedef struct +{ + az_span name; ///< Name. + az_span value; ///< Value. +} _az_http_request_header; + +/** + * @brief A type representing a buffer of #_az_http_request_header instances for HTTP request + * headers. + */ +typedef az_span _az_http_request_headers; + +/** + * @brief Structure used to represent an HTTP request. + * It contains an HTTP method, URL, headers and body. It also contains + * another utility variables. + */ +typedef struct +{ + struct + { + az_context* context; + az_http_method method; + az_span url; + int32_t url_length; + int32_t query_start; + _az_http_request_headers headers; // Contains instances of _az_http_request_header + int32_t headers_length; + int32_t max_headers; + int32_t retry_headers_start_byte_offset; + az_span body; + } _internal; +} az_http_request; + +/** + * @brief Used to declare policy process callback #_az_http_policy_process_fn definition. + */ +// Definition is below. +typedef struct _az_http_policy _az_http_policy; + +/** + * @brief Defines the callback signature of a policy process which should receive an + * #_az_http_policy, options reference (as `void*`), an #az_http_request and an #az_http_response. + * + * @remark `void*` is used as polymorphic solution for any policy. Each policy implementation would + * know the specific pointer type to cast options to. + */ +typedef AZ_NODISCARD az_result (*_az_http_policy_process_fn)( + _az_http_policy* ref_policies, + void* ref_options, + az_http_request* ref_request, + az_http_response* ref_response); + +/** + * @brief HTTP policy. + * An HTTP pipeline inside SDK clients is an array of HTTP policies. + */ +struct _az_http_policy +{ + struct + { + _az_http_policy_process_fn process; + void* options; + } _internal; +}; + +/** + * @brief Gets the HTTP header by index. + * + * @param[in] request HTTP request to get HTTP header from. + * @param[in] index Index of the HTTP header to get. + * @param[out] out_name A pointer to an #az_span to write the header's name. + * @param[out] out_value A pointer to an #az_span to write the header's value. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_ARG \p index is out of range. + */ +AZ_NODISCARD az_result az_http_request_get_header( + az_http_request const* request, + int32_t index, + az_span* out_name, + az_span* out_value); + +/** + * @brief Get method of an HTTP request. + * + * @remarks This function is expected to be used by transport layer only. + * + * @param[in] request The HTTP request from which to get the method. + * @param[out] out_method Pointer to write the HTTP method to. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval other Failure. + */ +AZ_NODISCARD az_result +az_http_request_get_method(az_http_request const* request, az_http_method* out_method); + +/** + * @brief Get the URL from an HTTP request. + * + * @remarks This function is expected to be used by transport layer only. + * + * @param[in] request The HTTP request from which to get the URL. + * @param[out] out_url Pointer to write the HTTP URL to. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval other Failure. + */ +AZ_NODISCARD az_result az_http_request_get_url(az_http_request const* request, az_span* out_url); + +/** + * @brief Get body from an HTTP request. + * + * @remarks This function is expected to be used by transport layer only. + * + * @param[in] request The HTTP request from which to get the body. + * @param[out] out_body Pointer to write the HTTP request body to. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval other Failure. + */ +AZ_NODISCARD az_result az_http_request_get_body(az_http_request const* request, az_span* out_body); + +/** + * @brief This function is expected to be used by transport adapters like curl. Use it to write + * content from \p source to \p ref_response. + * + * @remarks The \p source can be an empty #az_span. If so, nothing will be written. + * + * @param[in,out] ref_response Pointer to an #az_http_response. + * @param[in] source This is an #az_span with the content to be written into \p ref_response. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The \p response buffer is not big enough to contain the \p + * source content. + */ +AZ_NODISCARD az_result az_http_response_append(az_http_response* ref_response, az_span source); + +/** + * @brief Returns the number of headers within the request. + * + * @param[in] request Pointer to an #az_http_request to be used by this function. + * + * @return Number of headers in the \p request. + */ +AZ_NODISCARD int32_t az_http_request_headers_count(az_http_request const* request); + +/** + * @brief Sends an HTTP request through the wire and write the response into \p ref_response. + * + * @param[in] request Points to an #az_http_request that contains the settings and data that is + * used to send the request through the wire. + * @param[in,out] ref_response Points to an #az_http_response where the response from the wire will + * be written. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_HTTP_RESPONSE_OVERFLOW There was an issue while trying to write into \p + * ref_response. It might mean that there was not enough space in \p ref_response to hold the entire + * response from the network. + * @retval #AZ_ERROR_HTTP_RESPONSE_COULDNT_RESOLVE_HOST The URL from \p ref_request can't be + * resolved by the HTTP stack and the request was not sent. + * @retval #AZ_ERROR_HTTP_ADAPTER Any other issue from the transport adapter layer. + * @retval #AZ_ERROR_DEPENDENCY_NOT_PROVIDED No platform implementation was supplied to support this + * function. + */ +AZ_NODISCARD az_result +az_http_client_send_request(az_http_request const* request, az_http_response* ref_response); + +#include <_az_cfg_suffix.h> + +#endif // _az_HTTP_TRANSPORT_H diff --git a/src/az_iot.h b/src/az_iot.h new file mode 100644 index 00000000..92b49740 --- /dev/null +++ b/src/az_iot.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Azure IoT public headers. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_IOT_H +#define _az_IOT_H + +#include +#include +#include +#include + +#endif // _az_IOT_CORE_H diff --git a/src/az_iot_common.c b/src/az_iot_common.c new file mode 100644 index 00000000..39a66dd9 --- /dev/null +++ b/src/az_iot_common.c @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include <_az_cfg.h> + +static const az_span hub_client_param_separator_span = AZ_SPAN_LITERAL_FROM_STR("&"); +static const az_span hub_client_param_equals_span = AZ_SPAN_LITERAL_FROM_STR("="); + +AZ_NODISCARD az_result az_iot_message_properties_init( + az_iot_message_properties* properties, + az_span buffer, + int32_t written_length) +{ + _az_PRECONDITION_NOT_NULL(properties); + _az_PRECONDITION_VALID_SPAN(buffer, 0, true); + _az_PRECONDITION_RANGE(0, written_length, az_span_size(buffer)); + + properties->_internal.properties_buffer = buffer; + properties->_internal.properties_written = written_length; + properties->_internal.current_property_index = 0; + + return AZ_OK; +} + +AZ_NODISCARD az_result +az_iot_message_properties_append(az_iot_message_properties* properties, az_span name, az_span value) +{ + _az_PRECONDITION_NOT_NULL(properties); + _az_PRECONDITION_VALID_SPAN(name, 1, false); + _az_PRECONDITION_VALID_SPAN(value, 1, false); + + int32_t prop_length = properties->_internal.properties_written; + + az_span remainder = az_span_slice_to_end(properties->_internal.properties_buffer, prop_length); + + int32_t required_length = az_span_size(name) + az_span_size(value) + 1; + + if (prop_length > 0) + { + required_length += 1; + } + + _az_RETURN_IF_NOT_ENOUGH_SIZE(remainder, required_length); + + if (prop_length > 0) + { + remainder = az_span_copy_u8(remainder, *az_span_ptr(hub_client_param_separator_span)); + } + + remainder = az_span_copy(remainder, name); + remainder = az_span_copy_u8(remainder, *az_span_ptr(hub_client_param_equals_span)); + az_span_copy(remainder, value); + + properties->_internal.properties_written += required_length; + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_message_properties_find( + az_iot_message_properties* properties, + az_span name, + az_span* out_value) +{ + _az_PRECONDITION_NOT_NULL(properties); + _az_PRECONDITION_VALID_SPAN(name, 1, false); + _az_PRECONDITION_NOT_NULL(out_value); + + az_span remaining = az_span_slice( + properties->_internal.properties_buffer, 0, properties->_internal.properties_written); + + while (az_span_size(remaining) != 0) + { + int32_t index = 0; + az_span delim_span + = _az_span_token(remaining, hub_client_param_equals_span, &remaining, &index); + if (index != -1) + { + if (az_span_is_content_equal(delim_span, name)) + { + *out_value = _az_span_token(remaining, hub_client_param_separator_span, &remaining, &index); + return AZ_OK; + } + + _az_span_token(remaining, hub_client_param_separator_span, &remaining, &index); + } + } + + return AZ_ERROR_ITEM_NOT_FOUND; +} + +AZ_NODISCARD az_result az_iot_message_properties_next( + az_iot_message_properties* properties, + az_span* out_name, + az_span* out_value) +{ + _az_PRECONDITION_NOT_NULL(properties); + _az_PRECONDITION_NOT_NULL(out_name); + _az_PRECONDITION_NOT_NULL(out_value); + + int32_t index = (int32_t)properties->_internal.current_property_index; + int32_t prop_length = properties->_internal.properties_written; + + if (index == prop_length) + { + *out_name = AZ_SPAN_EMPTY; + *out_value = AZ_SPAN_EMPTY; + return AZ_ERROR_IOT_END_OF_PROPERTIES; + } + + az_span remainder; + az_span prop_span = az_span_slice(properties->_internal.properties_buffer, index, prop_length); + + int32_t location = 0; + *out_name = _az_span_token(prop_span, hub_client_param_equals_span, &remainder, &location); + *out_value = _az_span_token(remainder, hub_client_param_separator_span, &remainder, &location); + if (az_span_size(remainder) == 0) + { + properties->_internal.current_property_index = (uint32_t)prop_length; + } + else + { + properties->_internal.current_property_index += (uint32_t)(_az_span_diff(remainder, prop_span)); + } + + return AZ_OK; +} + +AZ_NODISCARD int32_t az_iot_calculate_retry_delay( + int32_t operation_msec, + int16_t attempt, + int32_t min_retry_delay_msec, + int32_t max_retry_delay_msec, + int32_t random_jitter_msec) +{ + _az_PRECONDITION_RANGE(0, operation_msec, INT32_MAX - 1); + _az_PRECONDITION_RANGE(0, attempt, INT16_MAX - 1); + _az_PRECONDITION_RANGE(0, min_retry_delay_msec, INT32_MAX - 1); + _az_PRECONDITION_RANGE(0, max_retry_delay_msec, INT32_MAX - 1); + _az_PRECONDITION_RANGE(0, random_jitter_msec, INT32_MAX - 1); + + if (_az_LOG_SHOULD_WRITE(AZ_LOG_IOT_RETRY)) + { + _az_LOG_WRITE(AZ_LOG_IOT_RETRY, AZ_SPAN_EMPTY); + } + + int32_t delay = _az_retry_calc_delay(attempt, min_retry_delay_msec, max_retry_delay_msec); + + if (delay < 0) + { + delay = max_retry_delay_msec; + } + + if (max_retry_delay_msec - delay > random_jitter_msec) + { + delay += random_jitter_msec; + } + + delay -= operation_msec; + + return delay > 0 ? delay : 0; +} + +AZ_NODISCARD int32_t _az_iot_u32toa_size(uint32_t number) +{ + if (number == 0) + { + return 1; + } + + uint32_t div = _az_SMALLEST_10_DIGIT_NUMBER; + int32_t digit_count = _az_MAX_SIZE_FOR_UINT32; + while (number / div == 0) + { + div /= _az_NUMBER_OF_DECIMAL_VALUES; + digit_count--; + } + + return digit_count; +} + +AZ_NODISCARD int32_t _az_iot_u64toa_size(uint64_t number) +{ + if (number == 0) + { + return 1; + } + + uint64_t div = _az_SMALLEST_20_DIGIT_NUMBER; + int32_t digit_count = _az_MAX_SIZE_FOR_UINT64; + while (number / div == 0) + { + div /= _az_NUMBER_OF_DECIMAL_VALUES; + digit_count--; + } + + return digit_count; +} + +AZ_NODISCARD az_result +_az_span_copy_url_encode(az_span destination, az_span source, az_span* out_remainder) +{ + int32_t length = 0; + _az_RETURN_IF_FAILED(_az_span_url_encode(destination, source, &length)); + *out_remainder = az_span_slice(destination, length, az_span_size(destination)); + return AZ_OK; +} diff --git a/src/az_iot_common.h b/src/az_iot_common.h new file mode 100644 index 00000000..d7f08aab --- /dev/null +++ b/src/az_iot_common.h @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file az_iot_common.h + * + * @brief Azure IoT common definitions. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_IOT_CORE_H +#define _az_IOT_CORE_H + +#include +#include +#include + +#include +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief The type represents the various #az_result success and error conditions specific to the + * IoT clients within the SDK. + */ +enum az_result_iot +{ + // === IoT error codes === + /// The IoT topic is not matching the expected format. + AZ_ERROR_IOT_TOPIC_NO_MATCH = _az_RESULT_MAKE_ERROR(_az_FACILITY_IOT, 1), + + /// While iterating, there are no more properties to return. + AZ_ERROR_IOT_END_OF_PROPERTIES = _az_RESULT_MAKE_ERROR(_az_FACILITY_IOT, 2), +}; + +/** + * @brief Identifies the #az_log_classification produced specifically by the IoT clients within the + * SDK. + */ +enum az_log_classification_iot +{ + AZ_LOG_MQTT_RECEIVED_TOPIC + = _az_LOG_MAKE_CLASSIFICATION(_az_FACILITY_IOT_MQTT, 1), ///< Accepted MQTT topic received. + + AZ_LOG_MQTT_RECEIVED_PAYLOAD + = _az_LOG_MAKE_CLASSIFICATION(_az_FACILITY_IOT_MQTT, 2), ///< Accepted MQTT payload received. + + AZ_LOG_IOT_RETRY = _az_LOG_MAKE_CLASSIFICATION(_az_FACILITY_IOT, 1), ///< IoT Client retry. + + AZ_LOG_IOT_SAS_TOKEN + = _az_LOG_MAKE_CLASSIFICATION(_az_FACILITY_IOT, 2), ///< IoT Client generated new SAS token. + + AZ_LOG_IOT_AZURERTOS + = _az_LOG_MAKE_CLASSIFICATION(_az_FACILITY_IOT, 3), ///< Azure IoT classification for Azure RTOS. +}; + +enum +{ + AZ_IOT_DEFAULT_MQTT_CONNECT_PORT = 8883, + AZ_IOT_DEFAULT_MQTT_CONNECT_KEEPALIVE_SECONDS = 240 +}; + +/** + * @brief Azure IoT service status codes. + * + * @note https://docs.microsoft.com/azure/iot-central/core/troubleshoot-connection#error-codes + * + */ +typedef enum +{ + // Default, unset value + AZ_IOT_STATUS_UNKNOWN = 0, + + // Service success codes + AZ_IOT_STATUS_OK = 200, + AZ_IOT_STATUS_ACCEPTED = 202, + AZ_IOT_STATUS_NO_CONTENT = 204, + + // Service error codes + AZ_IOT_STATUS_BAD_REQUEST = 400, + AZ_IOT_STATUS_UNAUTHORIZED = 401, + AZ_IOT_STATUS_FORBIDDEN = 403, + AZ_IOT_STATUS_NOT_FOUND = 404, + AZ_IOT_STATUS_NOT_ALLOWED = 405, + AZ_IOT_STATUS_NOT_CONFLICT = 409, + AZ_IOT_STATUS_PRECONDITION_FAILED = 412, + AZ_IOT_STATUS_REQUEST_TOO_LARGE = 413, + AZ_IOT_STATUS_UNSUPPORTED_TYPE = 415, + AZ_IOT_STATUS_THROTTLED = 429, + AZ_IOT_STATUS_CLIENT_CLOSED = 499, + AZ_IOT_STATUS_SERVER_ERROR = 500, + AZ_IOT_STATUS_BAD_GATEWAY = 502, + AZ_IOT_STATUS_SERVICE_UNAVAILABLE = 503, + AZ_IOT_STATUS_TIMEOUT = 504, +} az_iot_status; + +/* + * + * Properties APIs + * + * IoT message properties are used for Device-to-Cloud (D2C) as well as Cloud-to-Device (C2D). + * Properties are always appended to the MQTT topic of the published or received message and + * must contain percent-encoded names and values. + */ + +/// Add unique identification to a message. +/// @note It can be used with IoT message property APIs by wrapping the macro in a +/// #AZ_SPAN_FROM_STR macro as a parameter, where needed. +#define AZ_IOT_MESSAGE_PROPERTIES_MESSAGE_ID "%24.mid" + +/// Used in distributed tracing. +/// @note More information here: +/// https://docs.microsoft.com/azure/iot-hub/iot-hub-distributed-tracing. +/// @note It can be used with IoT message property APIs by wrapping the macro in a +/// #AZ_SPAN_FROM_STR macro as a parameter, where needed. +#define AZ_IOT_MESSAGE_PROPERTIES_CORRELATION_ID "%24.cid" + +/// URL encoded and of the form `text%2Fplain` or `application%2Fjson`, etc. +/// @note It can be used with IoT message property APIs by wrapping the macro in a +/// #AZ_SPAN_FROM_STR macro as a parameter, where needed. +#define AZ_IOT_MESSAGE_PROPERTIES_CONTENT_TYPE "%24.ct" + +/// UTF-8, UTF-16, etc. +/// @note It can be used with IoT message property APIs by wrapping the macro in a +/// #AZ_SPAN_FROM_STR macro as a parameter, where needed. +#define AZ_IOT_MESSAGE_PROPERTIES_CONTENT_ENCODING "%24.ce" + +/// User ID field. +/// @note It can be used with IoT message property APIs by wrapping the macro in a +/// #AZ_SPAN_FROM_STR macro as a parameter, where needed. +#define AZ_IOT_MESSAGE_PROPERTIES_USER_ID "%24.uid" + +/// Creation time of the message. +/// @note It can be used with IoT message property APIs by wrapping the macro in a +/// #AZ_SPAN_FROM_STR macro as a parameter, where needed. +#define AZ_IOT_MESSAGE_PROPERTIES_CREATION_TIME "%24.ctime" + +/// Name of the component +/// @note It can be used with IoT message property APIs by wrapping the macro in a +/// #AZ_SPAN_FROM_STR macro as a parameter, where needed. +#define AZ_IOT_MESSAGE_COMPONENT_NAME "%24.sub" + +/** + * @brief Telemetry or C2D properties. + * + */ +typedef struct +{ + struct + { + az_span properties_buffer; + int32_t properties_written; + uint32_t current_property_index; + } _internal; +} az_iot_message_properties; + +/** + * @brief Initializes the Telemetry or C2D properties. + * + * @note The properties init API will not encode properties. In order to support + * the following characters, they must be percent-encoded (RFC3986) as follows: + * - `/` : `%2F` + * - `%` : `%25` + * - `#` : `%23` + * - `&` : `%26` + * Only these characters would have to be encoded. If you would like to avoid the need to + * encode the names/values, avoid using these characters in names and values. + * + * @param[in] properties The #az_iot_message_properties to initialize. + * @param[in] buffer Can either be an unfilled (but properly sized) #az_span or an #az_span + * containing properly formatted (with above mentioned characters encoded if applicable) properties + * with the following format: {name}={value}&{name}={value}. + * @param[in] written_length The length of the properly formatted properties already initialized + * within the buffer. If the \p buffer is unfilled (uninitialized), this should be 0. + * @pre \p properties must not be `NULL`. + * @pre \p buffer must be a valid span of size greater than 0. + * @pre \p written_length must be greater than or equal to 0. + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result az_iot_message_properties_init( + az_iot_message_properties* properties, + az_span buffer, + int32_t written_length); + +/** + * @brief Appends a name-value property to the list of properties. + * + * @note The properties append API will not encode properties. In order to support + * the following characters, they must be percent-encoded (RFC3986) as follows: + * `/` : `%2F` + * `%` : `%25` + * `#` : `%23` + * `&` : `%26` + * Only these characters would have to be encoded. If you would like to avoid the need to + * encode the names/values, avoid using these characters in names and values. + * + * @param[in] properties The #az_iot_message_properties to use for this call. + * @param[in] name The name of the property. Must be a valid, non-empty span. + * @param[in] value The value of the property. Must be a valid, non-empty span. + * @pre \p properties must not be `NULL`. + * @pre \p name must be a valid span of size greater than 0. + * @pre \p value must be a valid span of size greater than 0. + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The operation was performed successfully. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE There was not enough space to append the property. + */ +AZ_NODISCARD az_result az_iot_message_properties_append( + az_iot_message_properties* properties, + az_span name, + az_span value); + +/** + * @brief Finds the value of a property. + * @remark This will return the first value of the property with the given name if multiple + * properties with the same name exist. + * + * @param[in] properties The #az_iot_message_properties to use for this call. + * @param[in] name The name of the property to search for. + * @param[out] out_value An #az_span containing the value of the found property. + * @pre \p properties must not be `NULL`. + * @pre \p name must be a valid span of size greater than 0. + * @pre \p out_value must not be `NULL`. + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The property was successfully found. + * @retval #AZ_ERROR_ITEM_NOT_FOUND The property could not be found. + */ +AZ_NODISCARD az_result az_iot_message_properties_find( + az_iot_message_properties* properties, + az_span name, + az_span* out_value); + +/** + * @brief Iterates over the list of properties. + * + * @param[in] properties The #az_iot_message_properties to use for this call. + * @param[out] out_name A pointer to an #az_span containing the name of the next property. + * @param[out] out_value A pointer to an #az_span containing the value of the next property. + * @pre \p properties must not be `NULL`. + * @pre \p out_name must not be `NULL`. + * @pre \p out_value must not be `NULL`. + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK A property was retrieved successfully. + * @retval #AZ_ERROR_IOT_END_OF_PROPERTIES The API reached the end of the properties to retrieve. + */ +AZ_NODISCARD az_result az_iot_message_properties_next( + az_iot_message_properties* properties, + az_span* out_name, + az_span* out_value); + +/** + * @brief Checks if the status indicates a successful operation. + * + * @param[in] status The #az_iot_status to verify. + * @return `true` if the status indicates success. `false` otherwise. + */ +AZ_NODISCARD AZ_INLINE bool az_iot_status_succeeded(az_iot_status status) +{ + return status < AZ_IOT_STATUS_BAD_REQUEST; +} + +/** + * @brief Checks if the status indicates a retriable error occurred during the + * operation. + * + * @param[in] status The #az_iot_status to verify. + * @return `true` if the operation should be retried. `false` otherwise. + */ +AZ_NODISCARD AZ_INLINE bool az_iot_status_retriable(az_iot_status status) +{ + return ((status == AZ_IOT_STATUS_THROTTLED) || (status == AZ_IOT_STATUS_SERVER_ERROR)); +} + +/** + * @brief Calculates the recommended delay before retrying an operation that failed. + * + * @param[in] operation_msec The time it took, in milliseconds, to perform the operation that + * failed. + * @param[in] attempt The number of failed retry attempts. + * @param[in] min_retry_delay_msec The minimum time, in milliseconds, to wait before a retry. + * @param[in] max_retry_delay_msec The maximum time, in milliseconds, to wait before a retry. + * @param[in] random_jitter_msec A random value between 0 and the maximum allowed jitter, in + * milliseconds. + * @pre \p operation_msec must be between 0 and INT32_MAX - 1. + * @pre \p attempt must be between 0 and INT16_MAX - 1. + * @pre \p min_retry_delay_msec must be between 0 and INT32_MAX - 1. + * @pre \p max_retry_delay_msec must be between 0 and INT32_MAX - 1. + * @pre \p random_jitter_msec must be between 0 and INT32_MAX - 1. + * @return The recommended delay in milliseconds. + */ +AZ_NODISCARD int32_t az_iot_calculate_retry_delay( + int32_t operation_msec, + int16_t attempt, + int32_t min_retry_delay_msec, + int32_t max_retry_delay_msec, + int32_t random_jitter_msec); + +#include <_az_cfg_suffix.h> + +#endif // _az_IOT_CORE_H diff --git a/src/az_iot_common_internal.h b/src/az_iot_common_internal.h new file mode 100644 index 00000000..083de7fc --- /dev/null +++ b/src/az_iot_common_internal.h @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file az_iot_common.h + * + * @brief Azure IoT common definitions. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_IOT_CORE_INTERNAL_H +#define _az_IOT_CORE_INTERNAL_H + +#include +#include + +#include +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief Gives the length, in bytes, of the string that would represent the given number. + * + * @param[in] number The number whose length, as a string, is to be evaluated. + * @return The length (not considering null terminator) of the string that would represent the given + * number. + */ +AZ_NODISCARD int32_t _az_iot_u32toa_size(uint32_t number); + +/** + * @brief Gives the length, in bytes, of the string that would represent the given number. + * + * @param[in] number The number whose length, as a string, is to be evaluated. + * @return The length (not considering null terminator) of the string that would represent the given + * number. + */ +AZ_NODISCARD int32_t _az_iot_u64toa_size(uint64_t number); + +/** + * @brief Copies the url-encoded content of `source` span into `destination`, returning the free + * remaining of `destination`. + * + * @param[in] destination The span where the `source` is url-encoded to. + * @param[in] source The span to url-encode and copy the content from. + * @param[out] out_remainder A slice of `destination` with the non-used buffer portion of + * `destination`. + * @return An `az_result` value. + */ +AZ_NODISCARD az_result +_az_span_copy_url_encode(az_span destination, az_span source, az_span* out_remainder); + +#include <_az_cfg_suffix.h> + +#endif // _az_IOT_CORE_INTERNAL_H diff --git a/src/az_iot_hub_client.c b/src/az_iot_hub_client.c new file mode 100644 index 00000000..0d301d1b --- /dev/null +++ b/src/az_iot_hub_client.c @@ -0,0 +1,175 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include <_az_cfg.h> + +static const uint8_t null_terminator = '\0'; +static const uint8_t hub_client_forward_slash = '/'; +static const az_span hub_client_param_separator_span = AZ_SPAN_LITERAL_FROM_STR("&"); +static const az_span hub_client_param_equals_span = AZ_SPAN_LITERAL_FROM_STR("="); + +static const az_span hub_digital_twin_model_id = AZ_SPAN_LITERAL_FROM_STR("model-id"); +static const az_span hub_service_api_version = AZ_SPAN_LITERAL_FROM_STR("/?api-version=2020-09-30"); +static const az_span client_sdk_version + = AZ_SPAN_LITERAL_FROM_STR("DeviceClientType=c%2F" AZ_SDK_VERSION_STRING); + +AZ_NODISCARD az_iot_hub_client_options az_iot_hub_client_options_default() +{ + return (az_iot_hub_client_options){ .module_id = AZ_SPAN_EMPTY, + .user_agent = client_sdk_version, + .model_id = AZ_SPAN_EMPTY, + .component_names = NULL, + .component_names_length = 0 }; +} + +AZ_NODISCARD az_result az_iot_hub_client_init( + az_iot_hub_client* client, + az_span iot_hub_hostname, + az_span device_id, + az_iot_hub_client_options const* options) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_VALID_SPAN(iot_hub_hostname, 1, false); + _az_PRECONDITION_VALID_SPAN(device_id, 1, false); + + client->_internal.iot_hub_hostname = iot_hub_hostname; + client->_internal.device_id = device_id; + client->_internal.options = options == NULL ? az_iot_hub_client_options_default() : *options; + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_hub_client_get_user_name( + az_iot_hub_client const* client, + char* mqtt_user_name, + size_t mqtt_user_name_size, + size_t* out_mqtt_user_name_length) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(mqtt_user_name); + _az_PRECONDITION(mqtt_user_name_size > 0); + + const az_span* const module_id = &(client->_internal.options.module_id); + const az_span* const user_agent = &(client->_internal.options.user_agent); + const az_span* const model_id = &(client->_internal.options.model_id); + + az_span mqtt_user_name_span + = az_span_create((uint8_t*)mqtt_user_name, (int32_t)mqtt_user_name_size); + + int32_t required_length = az_span_size(client->_internal.iot_hub_hostname) + + az_span_size(client->_internal.device_id) + (int32_t)sizeof(hub_client_forward_slash) + + az_span_size(hub_service_api_version); + if (az_span_size(*module_id) > 0) + { + required_length += az_span_size(*module_id) + (int32_t)sizeof(hub_client_forward_slash); + } + if (az_span_size(*user_agent) > 0) + { + required_length += az_span_size(*user_agent) + az_span_size(hub_client_param_separator_span); + } + // Note we skip the length of the model id since we have to url encode it. Bound checking is done + // later. + if (az_span_size(*model_id) > 0) + { + required_length += az_span_size(hub_client_param_separator_span) + + az_span_size(hub_client_param_equals_span); + } + + _az_RETURN_IF_NOT_ENOUGH_SIZE( + mqtt_user_name_span, required_length + (int32_t)sizeof(null_terminator)); + + az_span remainder = az_span_copy(mqtt_user_name_span, client->_internal.iot_hub_hostname); + remainder = az_span_copy_u8(remainder, hub_client_forward_slash); + remainder = az_span_copy(remainder, client->_internal.device_id); + + if (az_span_size(*module_id) > 0) + { + remainder = az_span_copy_u8(remainder, hub_client_forward_slash); + remainder = az_span_copy(remainder, *module_id); + } + + remainder = az_span_copy(remainder, hub_service_api_version); + + if (az_span_size(*user_agent) > 0) + { + remainder = az_span_copy_u8(remainder, *az_span_ptr(hub_client_param_separator_span)); + remainder = az_span_copy(remainder, *user_agent); + } + + if (az_span_size(*model_id) > 0) + { + remainder = az_span_copy_u8(remainder, *az_span_ptr(hub_client_param_separator_span)); + remainder = az_span_copy(remainder, hub_digital_twin_model_id); + remainder = az_span_copy_u8(remainder, *az_span_ptr(hub_client_param_equals_span)); + + _az_RETURN_IF_FAILED(_az_span_copy_url_encode(remainder, *model_id, &remainder)); + } + if (az_span_size(remainder) > 0) + { + remainder = az_span_copy_u8(remainder, null_terminator); + } + else + { + return AZ_ERROR_NOT_ENOUGH_SPACE; + } + + if (out_mqtt_user_name_length) + { + *out_mqtt_user_name_length + = mqtt_user_name_size - (size_t)az_span_size(remainder) - sizeof(null_terminator); + } + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_hub_client_get_client_id( + az_iot_hub_client const* client, + char* mqtt_client_id, + size_t mqtt_client_id_size, + size_t* out_mqtt_client_id_length) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(mqtt_client_id); + _az_PRECONDITION(mqtt_client_id_size > 0); + + az_span mqtt_client_id_span + = az_span_create((uint8_t*)mqtt_client_id, (int32_t)mqtt_client_id_size); + const az_span* const module_id = &(client->_internal.options.module_id); + + int32_t required_length = az_span_size(client->_internal.device_id); + if (az_span_size(*module_id) > 0) + { + required_length += az_span_size(*module_id) + (int32_t)sizeof(hub_client_forward_slash); + } + + _az_RETURN_IF_NOT_ENOUGH_SIZE( + mqtt_client_id_span, required_length + (int32_t)sizeof(null_terminator)); + + az_span remainder = az_span_copy(mqtt_client_id_span, client->_internal.device_id); + + if (az_span_size(*module_id) > 0) + { + remainder = az_span_copy_u8(remainder, hub_client_forward_slash); + remainder = az_span_copy(remainder, *module_id); + } + + az_span_copy_u8(remainder, null_terminator); + + if (out_mqtt_client_id_length) + { + *out_mqtt_client_id_length = (size_t)required_length; + } + + return AZ_OK; +} diff --git a/src/az_iot_hub_client.h b/src/az_iot_hub_client.h new file mode 100644 index 00000000..5fb3e6ba --- /dev/null +++ b/src/az_iot_hub_client.h @@ -0,0 +1,820 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file az_iot_hub_client.h + * + * @brief Definition for the Azure IoT Hub client. + * @note The IoT Hub MQTT protocol is described at + * https://docs.microsoft.com/azure/iot-hub/iot-hub-mqtt-support + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_IOT_HUB_CLIENT_H +#define _az_IOT_HUB_CLIENT_H + +#include +#include +#include + +#include +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief Azure IoT service MQTT bit field properties for telemetry publish messages. + * + */ +enum +{ + AZ_HUB_CLIENT_DEFAULT_MQTT_TELEMETRY_QOS = 0 +}; + +/** + * @brief Azure IoT Hub Client options. + * + */ +typedef struct +{ + /** + * The module name (if a module identity is used). + */ + az_span module_id; + + /** + * The user-agent is a formatted string that will be used for Azure IoT usage statistics. + */ + az_span user_agent; + + /** + * The model ID used to identify the capabilities of a device based on the Digital Twin document. + */ + az_span model_id; + + /** + * The array of component names for this device. + */ + az_span* component_names; + + /** + * The number of component names in the `component_names` array. + */ + int32_t component_names_length; +} az_iot_hub_client_options; + +/** + * @brief Azure IoT Hub Client. + */ +typedef struct +{ + struct + { + az_span iot_hub_hostname; + az_span device_id; + az_iot_hub_client_options options; + } _internal; +} az_iot_hub_client; + +/** + * @brief Gets the default Azure IoT Hub Client options. + * @details Call this to obtain an initialized #az_iot_hub_client_options structure that can be + * afterwards modified and passed to #az_iot_hub_client_init. + * + * @return #az_iot_hub_client_options. + */ +AZ_NODISCARD az_iot_hub_client_options az_iot_hub_client_options_default(); + +/** + * @brief Initializes an Azure IoT Hub Client. + * + * @param[out] client The #az_iot_hub_client to use for this call. + * @param[in] iot_hub_hostname The IoT Hub Hostname. + * @param[in] device_id The Device ID. If the ID contains any of the following characters, they must + * be percent-encoded as follows: + * - `/` : `%2F` + * - `%` : `%25` + * - `#` : `%23` + * - `&` : `%26` + * @param[in] options A reference to an #az_iot_hub_client_options structure. If `NULL` is passed, + * the hub client will use the default options. If using custom options, please initialize first by + * calling az_iot_hub_client_options_default() and then populating relevant options with your own + * values. + * @pre \p client must not be `NULL`. + * @pre \p iot_hub_hostname must be a valid span of size greater than 0. + * @pre \p device_id must be a valid span of size greater than 0. + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result az_iot_hub_client_init( + az_iot_hub_client* client, + az_span iot_hub_hostname, + az_span device_id, + az_iot_hub_client_options const* options); + +/** + * @brief The HTTP URI Path necessary when connecting to IoT Hub using WebSockets. + */ +#define AZ_IOT_HUB_CLIENT_WEB_SOCKET_PATH "/$iothub/websocket" + +/** + * @brief The HTTP URI Path necessary when connecting to IoT Hub using WebSockets without an X509 + * client certificate. + * @note Most devices should use #AZ_IOT_HUB_CLIENT_WEB_SOCKET_PATH. This option is available for + * devices not using X509 client certificates that fail to connect to IoT Hub. + */ +#define AZ_IOT_HUB_CLIENT_WEB_SOCKET_PATH_NO_X509_CLIENT_CERT \ + AZ_IOT_HUB_CLIENT_WEB_SOCKET_PATH "?iothub-no-client-cert=true" + +/** + * @brief Gets the MQTT user name. + * + * The user name will be of the following format: + * + * **Format without module ID** + * + * `{iothubhostname}/{device_id}/?api-version=2018-06-30&{user_agent}` + * + * **Format with module ID** + * + * `{iothubhostname}/{device_id}/{module_id}/?api-version=2018-06-30&{user_agent}` + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[out] mqtt_user_name A buffer with sufficient capacity to hold the MQTT user name. If + * successful, contains a null-terminated string with the user name that needs to be passed to the + * MQTT client. + * @param[in] mqtt_user_name_size The size, in bytes of \p mqtt_user_name. + * @param[out] out_mqtt_user_name_length __[nullable]__ Contains the string length, in bytes, of \p + * mqtt_user_name. Can be `NULL`. + * @pre \p client must not be `NULL`. + * @pre \p mqtt_user_name must not be `NULL`. + * @pre \p mqtt_user_name_size must be greater than 0. + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result az_iot_hub_client_get_user_name( + az_iot_hub_client const* client, + char* mqtt_user_name, + size_t mqtt_user_name_size, + size_t* out_mqtt_user_name_length); + +/** + * @brief Gets the MQTT client ID. + * + * The client ID will be of the following format: + * + * **Format without module ID** + * + * `{device_id}` + * + * **Format with module ID** + * + * `{device_id}/{module_id}` + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[out] mqtt_client_id A buffer with sufficient capacity to hold the MQTT client ID. If + * successful, contains a null-terminated string with the client ID that needs to be passed to the + * MQTT client. + * @param[in] mqtt_client_id_size The size, in bytes of \p mqtt_client_id. + * @param[out] out_mqtt_client_id_length __[nullable]__ Contains the string length, in bytes, of + * \p mqtt_client_id. Can be `NULL`. + * @pre \p client must not be `NULL`. + * @pre \p mqtt_client_id must not be `NULL`. + * @pre \p mqtt_client_id_size must be greater than 0. + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result az_iot_hub_client_get_client_id( + az_iot_hub_client const* client, + char* mqtt_client_id, + size_t mqtt_client_id_size, + size_t* out_mqtt_client_id_length); + +/* + * + * SAS Token APIs + * + * Use the following APIs when the Shared Access Key is available to the application or stored + * within a Hardware Security Module. The APIs are not necessary if X509 Client Certificate + * Authentication is used. + */ + +/** + * @brief Gets the Shared Access clear-text signature. + * @details The application must obtain a valid clear-text signature using this API, sign it using + * HMAC-SHA256 using the Shared Access Key as password then Base64 encode the result. + * + * Use the following APIs when the Shared Access Key is available to the application or stored + * within a Hardware Security Module. The APIs are not necessary if X509 Client Certificate + * Authentication is used. + * + * @note This API should be used in conjunction with az_iot_hub_client_sas_get_password(). + * + * @note More information available at + * https://docs.microsoft.com/azure/iot-hub/iot-hub-devguide-security#security-tokens + * + * A typical flow for using these two APIs might look something like the following (note the size + * of buffers and non-SDK APIs are for demo purposes only): + * + * @code + * const char* const signature_str = "TST+J9i1F8tE6dLYCtuQcu10u7umGO+aWGqPQhd9AAo="; + * az_span signature = AZ_SPAN_FROM_STR(signature_str); + * az_iot_hub_client_sas_get_signature(&client, expiration_time_in_seconds, signature, &signature); + * + * char decoded_sas_key[128] = { 0 }; + * base64_decode(base64_encoded_sas_key, decoded_sas_key); + * + * char signed_bytes[256] = { 0 }; + * hmac_256(az_span_ptr(signature), az_span_size(signature), decoded_sas_key, signed_bytes); + * + * char signed_bytes_base64_encoded[256] = { 0 }; + * base64_encode(signed_bytes, signed_bytes_base64_encoded); + * + * char final_password[512] = { 0 }; + * az_iot_hub_client_sas_get_password(client, expiration_time_in_seconds, + * AZ_SPAN_FROM_STR(signed_bytes_base64_encoded), final_password, sizeof(final_password), NULL); + * + * mqtt_set_password(&mqtt_client, final_password); + * @endcode + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in] token_expiration_epoch_time The time, in seconds, from 1/1/1970. + * @param[in] signature An empty #az_span with sufficient capacity to hold the SAS signature. + * @param[out] out_signature The output #az_span containing the SAS signature. + * @pre \p client must not be `NULL`. + * @pre \p token_expiration_epoch_time must be greater than 0. + * @pre \p signature must be a valid span of size greater than 0. + * @pre \p out_signature must not be `NULL`. + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result az_iot_hub_client_sas_get_signature( + az_iot_hub_client const* client, + uint64_t token_expiration_epoch_time, + az_span signature, + az_span* out_signature); + +/** + * @brief Gets the MQTT password. + * @note The MQTT password must be an empty string if X509 Client certificates are used. Use this + * API only when authenticating with SAS tokens. + * + * @note This API should be used in conjunction with az_iot_hub_client_sas_get_signature(). + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in] base64_hmac_sha256_signature The Base64 encoded value of the HMAC-SHA256(signature, + * SharedAccessKey). The signature is obtained by using az_iot_hub_client_sas_get_signature(). + * @param[in] token_expiration_epoch_time The time, in seconds, from 1/1/1970. It MUST be the same + * value passed to az_iot_hub_client_sas_get_signature(). + * @param[in] key_name The Shared Access Key Name (Policy Name). This is optional. For security + * reasons we recommend using one key per device instead of using a global policy key. + * @param[out] mqtt_password A char buffer with sufficient capacity to hold the MQTT password. + * @param[in] mqtt_password_size The size, in bytes of \p mqtt_password. + * @param[out] out_mqtt_password_length __[nullable]__ Contains the string length, in bytes, of \p + * mqtt_password. Can be `NULL`. + * @pre \p client must not be `NULL`. + * @pre \p token_expiration_epoch_time must be greater than 0. + * @pre \p base64_hmac_sha256_signature must be a valid span of size greater than 0. + * @pre \p mqtt_password must not be `NULL`. + * @pre \p mqtt_password_size must be greater than 0. + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The operation was successful. In this case, \p mqtt_password will contain a + * null-terminated string with the password that needs to be passed to the MQTT client. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The \p mqtt_password does not have enough size. + */ +AZ_NODISCARD az_result az_iot_hub_client_sas_get_password( + az_iot_hub_client const* client, + uint64_t token_expiration_epoch_time, + az_span base64_hmac_sha256_signature, + az_span key_name, + char* mqtt_password, + size_t mqtt_password_size, + size_t* out_mqtt_password_length); + +/* + * + * Telemetry APIs + * + */ + +/** + * @brief Gets the MQTT topic that must be used for device to cloud telemetry messages. + * @note This topic can also be used to set the MQTT Will message in the Connect message. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in] properties An optional #az_iot_message_properties object (can be NULL). + * @param[out] mqtt_topic A buffer with sufficient capacity to hold the MQTT topic. If successful, + * contains a null-terminated string with the topic that needs to be passed to the MQTT client. + * @param[in] mqtt_topic_size The size, in bytes of \p mqtt_topic. + * @param[out] out_mqtt_topic_length __[nullable]__ Contains the string length, in bytes, of \p + * mqtt_topic. Can be `NULL`. + * @pre \p client must not be `NULL`. + * @pre \p mqtt_topic must not be `NULL`. + * @pre \p mqtt_topic_size must be greater than 0. + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The topic was retrieved successfully. + */ +AZ_NODISCARD az_result az_iot_hub_client_telemetry_get_publish_topic( + az_iot_hub_client const* client, + az_iot_message_properties const* properties, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length); + +/* + * + * Cloud-to-device (C2D) APIs + * + */ + +/** + * @brief The MQTT topic filter to subscribe to Cloud-to-Device requests. + * @note C2D MQTT Publish messages will have QoS At least once (1). + */ +#define AZ_IOT_HUB_CLIENT_C2D_SUBSCRIBE_TOPIC "devices/+/messages/devicebound/#" + +/** + * @brief The Cloud-To-Device Request. + * + */ +typedef struct +{ + /** + * The properties associated with this C2D request. + */ + az_iot_message_properties properties; +} az_iot_hub_client_c2d_request; + +/** + * @brief Attempts to parse a received message's topic for C2D features. + * + * @warning The topic must be a valid MQTT topic or the resulting behavior will be undefined. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in] received_topic An #az_span containing the received topic. + * @param[out] out_request If the message is a C2D request, this will contain the + * #az_iot_hub_client_c2d_request. + * @pre \p client must not be `NULL` and must already be initialized by first calling + * az_iot_hub_client_init(). + * @pre \p received_topic must be a valid span of size greater than 0. + * @pre \p out_request must not be `NULL`. + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The topic is meant for this feature and the \p out_request was populated + * with relevant information. + * @retval #AZ_ERROR_IOT_TOPIC_NO_MATCH The topic does not match the expected format. This could + * be due to either a malformed topic OR the message which came in on this topic is not meant for + * this feature. + */ +AZ_NODISCARD az_result az_iot_hub_client_c2d_parse_received_topic( + az_iot_hub_client const* client, + az_span received_topic, + az_iot_hub_client_c2d_request* out_request); + +/* + * + * Methods APIs + * + */ + +/** + * @brief The MQTT topic filter to subscribe to method requests. + * @note Methods MQTT Publish messages will have QoS At most once (0). + */ +#define AZ_IOT_HUB_CLIENT_METHODS_SUBSCRIBE_TOPIC "$iothub/methods/POST/#" + +/** + * @brief A method request received from IoT Hub. + * + */ +typedef struct +{ + /** + * The request ID. + * @note The application must match the method request and method response. + */ + az_span request_id; + + /** + * The method name. + */ + az_span name; +} az_iot_hub_client_method_request; + +/** + * @brief Attempts to parse a received message's topic for method features. + * + * @warning The topic must be a valid MQTT topic or the resulting behavior will be undefined. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in] received_topic An #az_span containing the received topic. + * @param[out] out_request If the message is a method request, this will contain the + * #az_iot_hub_client_method_request. + * @pre \p client must not be `NULL` and must already be initialized by first calling + * az_iot_hub_client_init(). + * @pre \p received_topic must be a valid span of size greater than 0. + * @pre \p out_request must not be `NULL`. + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The topic is meant for this feature and the \p out_request was populated + * with relevant information. + * @retval #AZ_ERROR_IOT_TOPIC_NO_MATCH The topic does not match the expected format. This could + * be due to either a malformed topic OR the message which came in on this topic is not meant for + * this feature. + */ +AZ_NODISCARD az_result az_iot_hub_client_methods_parse_received_topic( + az_iot_hub_client const* client, + az_span received_topic, + az_iot_hub_client_method_request* out_request); + +/** + * @brief Gets the MQTT topic that must be used to respond to method requests. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in] request_id The request ID. Must match a received #az_iot_hub_client_method_request + * request_id. + * @param[in] status A code that indicates the result of the method, as defined by the user. + * @param[out] mqtt_topic A buffer with sufficient capacity to hold the MQTT topic. If successful, + * contains a null-terminated string with the topic that needs to be passed to the MQTT client. + * @param[in] mqtt_topic_size The size, in bytes of \p mqtt_topic. + * @param[out] out_mqtt_topic_length __[nullable]__ Contains the string length, in bytes, of \p + * mqtt_topic. Can be `NULL`. + * @pre \p client must not be `NULL` and must already be initialized by first calling + * az_iot_hub_client_init(). + * @pre \p request_id must be a valid span of size greater than 0. + * @pre \p mqtt_topic must not be `NULL`. + * @pre \p mqtt_topic_size must be greater than 0. + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The topic was retrieved successfully. + */ +AZ_NODISCARD az_result az_iot_hub_client_methods_response_get_publish_topic( + az_iot_hub_client const* client, + az_span request_id, + uint16_t status, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length); + +/* + * + * Commands APIs + * + */ + +/** + * @brief The MQTT topic filter to subscribe to command requests. + * @note Commands MQTT Publish messages will have QoS At most once (0). + */ +#define AZ_IOT_HUB_CLIENT_COMMANDS_SUBSCRIBE_TOPIC AZ_IOT_HUB_CLIENT_METHODS_SUBSCRIBE_TOPIC + +/** + * @brief A command request received from IoT Hub. + * + */ +typedef struct +{ + /** + * The request ID. + * @note The application must match the command request and command response. + */ + az_span request_id; + + /** + * The name of the component which the command was invoked for. + * @note Can be `AZ_SPAN_EMPTY` if for the root component. + */ + az_span component_name; + + /** + * The command name. + */ + az_span command_name; +} az_iot_hub_client_command_request; + +/** + * @brief Attempts to parse a received message's topic for command features. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in] received_topic An #az_span containing the received topic. + * @param[out] out_request If the message is a command request, this will contain the + * #az_iot_hub_client_command_request. + * + * @pre \p client must not be `NULL`. + * @pre \p received_topic must be a valid, non-empty #az_span. + * @pre \p out_request must not be `NULL`. It must point to an #az_iot_hub_client_command_request + * instance. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The topic is meant for this feature and the \p out_request was populated + * with relevant information. + * @retval #AZ_ERROR_IOT_TOPIC_NO_MATCH The topic does not match the expected format. This could + * be due to either a malformed topic OR the message which came in on this topic is not meant for + * this feature. + */ +AZ_NODISCARD az_result az_iot_hub_client_commands_parse_received_topic( + az_iot_hub_client const* client, + az_span received_topic, + az_iot_hub_client_command_request* out_request); + +/** + * @brief Gets the MQTT topic that is used to respond to command requests. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in] request_id The request ID. Must match a received #az_iot_hub_client_command_request + * request_id. + * @param[in] status A code that indicates the result of the command, as defined by the application. + * @param[out] mqtt_topic A buffer with sufficient capacity to hold the MQTT topic. If successful, + * contains a null-terminated string with the topic that needs to be passed to the MQTT client. + * @param[in] mqtt_topic_size The size, in bytes, of \p mqtt_topic. + * @param[out] out_mqtt_topic_length __[nullable]__ Contains the string length, in bytes, of \p + * mqtt_topic. Can be `NULL`. + * + * @pre \p client must not be `NULL`. + * @pre \p request_id must be a valid, non-empty #az_span. + * @pre \p mqtt_topic must not be `NULL`. + * @pre \p mqtt_topic_size must be greater than 0. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The topic was retrieved successfully. + */ +AZ_NODISCARD az_result az_iot_hub_client_commands_response_get_publish_topic( + az_iot_hub_client const* client, + az_span request_id, + uint16_t status, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length); + +/* + * + * Twin APIs + * + */ + +/** + * @brief The MQTT topic filter to subscribe to twin operation responses. + * @note Twin MQTT Publish messages will have QoS At most once (0). + */ +#define AZ_IOT_HUB_CLIENT_TWIN_RESPONSE_SUBSCRIBE_TOPIC "$iothub/twin/res/#" + +/** + * @brief Gets the MQTT topic filter to subscribe to twin desired property changes. + * @note Twin MQTT Publish messages will have QoS At most once (0). + */ +#define AZ_IOT_HUB_CLIENT_TWIN_PATCH_SUBSCRIBE_TOPIC "$iothub/twin/PATCH/properties/desired/#" + +/** + * @brief Twin response type. + * + */ +typedef enum +{ + AZ_IOT_HUB_CLIENT_TWIN_RESPONSE_TYPE_GET = 1, + AZ_IOT_HUB_CLIENT_TWIN_RESPONSE_TYPE_DESIRED_PROPERTIES = 2, + AZ_IOT_HUB_CLIENT_TWIN_RESPONSE_TYPE_REPORTED_PROPERTIES = 3, + AZ_IOT_HUB_CLIENT_TWIN_RESPONSE_TYPE_REQUEST_ERROR = 4, +} az_iot_hub_client_twin_response_type; + +/** + * @brief Twin response. + * + */ +typedef struct +{ + /** + * Request ID matches the ID specified when issuing a Get or Patch command. + */ + az_span request_id; + + // Avoid using enum as the first field within structs, to allow for { 0 } initialization. + // This is a workaround for IAR compiler warning [Pe188]: enumerated type mixed with another type. + + /** + * Twin response type. + */ + az_iot_hub_client_twin_response_type response_type; + + /** + * The operation status. + */ + az_iot_status status; + + /** + * The Twin object version. + * @note This is only returned when + * `response_type == AZ_IOT_CLIENT_TWIN_RESPONSE_TYPE_DESIRED_PROPERTIES` + * or + * `response_type == AZ_IOT_CLIENT_TWIN_RESPONSE_TYPE_REPORTED_PROPERTIES`. + */ + az_span version; +} az_iot_hub_client_twin_response; + +/** + * @brief Attempts to parse a received message's topic for twin features. + * + * @warning The topic must be a valid MQTT topic or the resulting behavior will be undefined. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in] received_topic An #az_span containing the received topic. + * @param[out] out_response If the message is twin-operation related, this will contain the + * #az_iot_hub_client_twin_response. + * @pre \p client must not be `NULL` and must already be initialized by first calling + * az_iot_hub_client_init(). + * @pre \p received_topic must be a valid span of size greater than 0. + * @pre \p out_response must not be `NULL`. + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The topic is meant for this feature and the \p out_response was populated + * with relevant information. + * @retval #AZ_ERROR_IOT_TOPIC_NO_MATCH The topic does not match the expected format. This could + * be due to either a malformed topic OR the message which came in on this topic is not meant for + * this feature. + */ +AZ_NODISCARD az_result az_iot_hub_client_twin_parse_received_topic( + az_iot_hub_client const* client, + az_span received_topic, + az_iot_hub_client_twin_response* out_response); + +/** + * @brief Gets the MQTT topic that must be used to submit a Twin GET request. + * @note The payload of the MQTT publish message should be empty. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in] request_id The request ID. + * @param[out] mqtt_topic A buffer with sufficient capacity to hold the MQTT topic. If successful, + * contains a null-terminated string with the topic that needs to be passed to the MQTT client. + * @param[in] mqtt_topic_size The size, in bytes of \p mqtt_topic. + * @param[out] out_mqtt_topic_length __[nullable]__ Contains the string length, in bytes, of \p + * mqtt_topic. Can be `NULL`. + * @pre \p client must not be `NULL` and must already be initialized by first calling + * az_iot_hub_client_init(). + * @pre \p request_id must be a valid span of size greater than 0. + * @pre \p mqtt_topic must not be `NULL`. + * @pre \p mqtt_topic_size must be greater than 0. + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The topic was retrieved successfully. + */ +AZ_NODISCARD az_result az_iot_hub_client_twin_document_get_publish_topic( + az_iot_hub_client const* client, + az_span request_id, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length); + +/** + * @brief Gets the MQTT topic that must be used to submit a Twin PATCH request. + * @note The payload of the MQTT publish message should contain a JSON document formatted according + * to the Twin specification. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in] request_id The request ID. + * @param[out] mqtt_topic A buffer with sufficient capacity to hold the MQTT topic. If successful, + * contains a null-terminated string with the topic that needs to be passed to the MQTT client. + * @param[in] mqtt_topic_size The size, in bytes of \p mqtt_topic. + * @param[out] out_mqtt_topic_length __[nullable]__ Contains the string length, in bytes, of \p + * mqtt_topic. Can be `NULL`. + * @pre \p client must not be `NULL` and must already be initialized by first calling + * az_iot_hub_client_init(). + * @pre \p request_id must be a valid span of size greater than 0. + * @pre \p mqtt_topic must not be `NULL`. + * @pre \p mqtt_topic_size must be greater than 0. + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The topic was retrieved successfully. + */ +AZ_NODISCARD az_result az_iot_hub_client_twin_patch_get_publish_topic( + az_iot_hub_client const* client, + az_span request_id, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length); + +/* + * + * Properties APIs + * + */ + +/** + * @brief The MQTT topic filter to subscribe to properties operation messages. + * @note Twin MQTT Publish messages will have QoS At most once (0). + */ +#define AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_SUBSCRIBE_TOPIC \ + AZ_IOT_HUB_CLIENT_TWIN_RESPONSE_SUBSCRIBE_TOPIC + +/** + * @brief Gets the MQTT topic filter to subscribe to desired properties changes. + * @note Property MQTT Publish messages will have QoS At most once (0). + */ +#define AZ_IOT_HUB_CLIENT_PROPERTIES_WRITABLE_UPDATES_SUBSCRIBE_TOPIC \ + AZ_IOT_HUB_CLIENT_TWIN_PATCH_SUBSCRIBE_TOPIC + +/** + * @brief Properties message type. + * + */ +typedef enum +{ + AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE + = 1, /**< A response from a properties "GET" request. */ + AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_WRITABLE_UPDATED + = 2, /**< A message with a payload containing updated writable properties for the device to + process. */ + AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_ACKNOWLEDGEMENT + = 3, /**< A response acknowledging the service has received properties that the device sent. */ + AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_ERROR + = 4, /**< An error has occurred from the service processing properties. */ +} az_iot_hub_client_properties_message_type; + +/** + * @brief Properties message. + * + */ +typedef struct +{ + az_iot_hub_client_properties_message_type message_type; /**< Properties message type. */ + az_iot_status status; /**< The operation status. */ + az_span request_id; /**< Request ID matches the ID specified when issuing the initial request to + properties. */ +} az_iot_hub_client_properties_message; + +/** + * @brief Attempts to parse a received message's topic for properties features. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in] received_topic An #az_span containing the received topic. + * @param[out] out_message If the message is properties-operation related, this will contain the + * #az_iot_hub_client_properties_message. + * + * @pre \p client must not be `NULL`. + * @pre \p received_topic must be a valid, non-empty #az_span. + * @pre \p out_message must not be `NULL`. It must point to an + * #az_iot_hub_client_properties_message instance. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The topic is meant for this feature and the \p out_message was populated + * with relevant information. + * @retval #AZ_ERROR_IOT_TOPIC_NO_MATCH The topic does not match the expected format. This could + * be due to either a malformed topic OR the message which came in on this topic is not meant for + * this feature. + */ +AZ_NODISCARD az_result az_iot_hub_client_properties_parse_received_topic( + az_iot_hub_client const* client, + az_span received_topic, + az_iot_hub_client_properties_message* out_message); + +/** + * @brief Gets the MQTT topic that is used to submit a properties GET request. + * @note The payload of the MQTT publish message should be empty. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in] request_id The request ID. + * @param[out] mqtt_topic A buffer with sufficient capacity to hold the MQTT topic. If + * successful, contains a null-terminated string with the topic that + * needs to be passed to the MQTT client. + * @param[in] mqtt_topic_size The size, in bytes, of \p mqtt_topic. + * @param[out] out_mqtt_topic_length __[nullable]__ Contains the string length, in bytes, of + * \p mqtt_topic. Can be `NULL`. + * @pre \p client must not be `NULL`. + * @pre \p request_id must be a valid, non-empty #az_span. + * @pre \p mqtt_topic must not be `NULL`. + * @pre \p mqtt_topic_size must be greater than 0. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The topic was retrieved successfully. + */ +AZ_NODISCARD az_result az_iot_hub_client_properties_document_get_publish_topic( + az_iot_hub_client const* client, + az_span request_id, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length); + +/** + * @brief Gets the MQTT topic that is used to send properties from the device to service. + * @note The payload of the MQTT publish message should contain a JSON document formatted according + * to the DTDL specification. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in] request_id The request ID. + * @param[out] mqtt_topic A buffer with sufficient capacity to hold the MQTT topic. If + * successful, contains a null-terminated string with the topic that + * needs to be passed to the MQTT client. + * @param[in] mqtt_topic_size The size, in bytes, of \p mqtt_topic. + * @param[out] out_mqtt_topic_length __[nullable]__ Contains the string length, in bytes, of + * \p mqtt_topic. Can be `NULL`. + * + * @pre \p client must not be `NULL`. + * @pre \p request_id must be a valid, non-empty #az_span. + * @pre \p mqtt_topic must not be `NULL`. + * @pre \p mqtt_topic_size must be greater than 0. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The topic was retrieved successfully. + */ +AZ_NODISCARD az_result az_iot_hub_client_properties_get_reported_publish_topic( + az_iot_hub_client const* client, + az_span request_id, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length); + +#include <_az_cfg_suffix.h> + +#endif // _az_IOT_HUB_CLIENT_H diff --git a/src/az_iot_hub_client_c2d.c b/src/az_iot_hub_client_c2d.c new file mode 100644 index 00000000..d64cd205 --- /dev/null +++ b/src/az_iot_hub_client_c2d.c @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include <_az_cfg.h> + +static const az_span c2d_topic_suffix = AZ_SPAN_LITERAL_FROM_STR("/messages/devicebound/"); + +AZ_NODISCARD az_result az_iot_hub_client_c2d_parse_received_topic( + az_iot_hub_client const* client, + az_span received_topic, + az_iot_hub_client_c2d_request* out_request) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_VALID_SPAN(client->_internal.iot_hub_hostname, 1, false); + _az_PRECONDITION_VALID_SPAN(received_topic, 1, false); + _az_PRECONDITION_NOT_NULL(out_request); + (void)client; + + int32_t index = 0; + az_span remainder; + (void)_az_span_token(received_topic, c2d_topic_suffix, &remainder, &index); + if (index == -1) + { + return AZ_ERROR_IOT_TOPIC_NO_MATCH; + } + + if (_az_LOG_SHOULD_WRITE(AZ_LOG_MQTT_RECEIVED_TOPIC)) + { + _az_LOG_WRITE(AZ_LOG_MQTT_RECEIVED_TOPIC, received_topic); + } + + az_span token = az_span_size(remainder) == 0 + ? AZ_SPAN_EMPTY + : _az_span_token(remainder, c2d_topic_suffix, &remainder, &index); + + _az_RETURN_IF_FAILED( + az_iot_message_properties_init(&out_request->properties, token, az_span_size(token))); + + return AZ_OK; +} diff --git a/src/az_iot_hub_client_commands.c b/src/az_iot_hub_client_commands.c new file mode 100644 index 00000000..3ff47b3d --- /dev/null +++ b/src/az_iot_hub_client_commands.c @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include <_az_cfg.h> + +static const az_span command_separator = AZ_SPAN_LITERAL_FROM_STR("*"); + +AZ_NODISCARD az_result az_iot_hub_client_commands_response_get_publish_topic( + az_iot_hub_client const* client, + az_span request_id, + uint16_t status, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length) +{ + return az_iot_hub_client_methods_response_get_publish_topic( + client, request_id, status, mqtt_topic, mqtt_topic_size, out_mqtt_topic_length); +} + +AZ_NODISCARD az_result az_iot_hub_client_commands_parse_received_topic( + az_iot_hub_client const* client, + az_span received_topic, + az_iot_hub_client_command_request* out_request) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_VALID_SPAN(received_topic, 1, false); + _az_PRECONDITION_NOT_NULL(out_request); + + az_iot_hub_client_method_request method_request; + + _az_RETURN_IF_FAILED( + az_iot_hub_client_methods_parse_received_topic(client, received_topic, &method_request)); + + out_request->request_id = method_request.request_id; + + int32_t command_separator_index = az_span_find(method_request.name, command_separator); + if (command_separator_index > 0) + { + out_request->component_name = az_span_slice(method_request.name, 0, command_separator_index); + out_request->command_name = az_span_slice( + method_request.name, command_separator_index + 1, az_span_size(method_request.name)); + } + else + { + out_request->component_name = AZ_SPAN_EMPTY; + out_request->command_name + = az_span_slice(method_request.name, 0, az_span_size(method_request.name)); + } + + return AZ_OK; +} diff --git a/src/az_iot_hub_client_methods.c b/src/az_iot_hub_client_methods.c new file mode 100644 index 00000000..4d564c19 --- /dev/null +++ b/src/az_iot_hub_client_methods.c @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include <_az_cfg.h> + +static const uint8_t null_terminator = '\0'; +static const az_span methods_topic_prefix = AZ_SPAN_LITERAL_FROM_STR("$iothub/methods/"); +static const az_span methods_topic_filter_suffix = AZ_SPAN_LITERAL_FROM_STR("POST/"); +static const az_span methods_response_topic_result = AZ_SPAN_LITERAL_FROM_STR("res/"); +static const az_span methods_response_topic_properties = AZ_SPAN_LITERAL_FROM_STR("/?$rid="); + +AZ_NODISCARD az_result az_iot_hub_client_methods_parse_received_topic( + az_iot_hub_client const* client, + az_span received_topic, + az_iot_hub_client_method_request* out_request) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_VALID_SPAN(client->_internal.iot_hub_hostname, 1, false); + _az_PRECONDITION_VALID_SPAN(received_topic, 1, false); + _az_PRECONDITION_NOT_NULL(out_request); + + (void)client; + + int32_t index = az_span_find(received_topic, methods_topic_prefix); + + if (index == -1) + { + return AZ_ERROR_IOT_TOPIC_NO_MATCH; + } + + if (_az_LOG_SHOULD_WRITE(AZ_LOG_MQTT_RECEIVED_TOPIC)) + { + _az_LOG_WRITE(AZ_LOG_MQTT_RECEIVED_TOPIC, received_topic); + } + + received_topic = az_span_slice( + received_topic, index + az_span_size(methods_topic_prefix), az_span_size(received_topic)); + + index = az_span_find(received_topic, methods_topic_filter_suffix); + + if (index == -1) + { + return AZ_ERROR_IOT_TOPIC_NO_MATCH; + } + + received_topic = az_span_slice( + received_topic, + index + az_span_size(methods_topic_filter_suffix), + az_span_size(received_topic)); + + index = az_span_find(received_topic, methods_response_topic_properties); + + if (index == -1) + { + return AZ_ERROR_IOT_TOPIC_NO_MATCH; + } + + out_request->name = az_span_slice(received_topic, 0, index); + out_request->request_id = az_span_slice( + received_topic, + index + az_span_size(methods_response_topic_properties), + az_span_size(received_topic)); + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_hub_client_methods_response_get_publish_topic( + az_iot_hub_client const* client, + az_span request_id, + uint16_t status, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_VALID_SPAN(client->_internal.iot_hub_hostname, 1, false); + _az_PRECONDITION_VALID_SPAN(request_id, 1, false); + _az_PRECONDITION_NOT_NULL(mqtt_topic); + _az_PRECONDITION(mqtt_topic_size > 0); + + (void)client; + + az_span mqtt_topic_span = az_span_create((uint8_t*)mqtt_topic, (int32_t)mqtt_topic_size); + int32_t required_length = az_span_size(methods_topic_prefix) + + az_span_size(methods_response_topic_result) + _az_iot_u32toa_size(status) + + az_span_size(methods_response_topic_properties) + az_span_size(request_id); + + _az_RETURN_IF_NOT_ENOUGH_SIZE( + mqtt_topic_span, required_length + (int32_t)sizeof(null_terminator)); + + az_span remainder = az_span_copy(mqtt_topic_span, methods_topic_prefix); + remainder = az_span_copy(remainder, methods_response_topic_result); + + _az_RETURN_IF_FAILED(az_span_u32toa(remainder, (uint32_t)status, &remainder)); + + remainder = az_span_copy(remainder, methods_response_topic_properties); + remainder = az_span_copy(remainder, request_id); + az_span_copy_u8(remainder, null_terminator); + + if (out_mqtt_topic_length) + { + *out_mqtt_topic_length = (size_t)required_length; + } + + return AZ_OK; +} diff --git a/src/az_iot_hub_client_properties.c b/src/az_iot_hub_client_properties.c new file mode 100644 index 00000000..9487aee2 --- /dev/null +++ b/src/az_iot_hub_client_properties.c @@ -0,0 +1,491 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include + +#include +#include + +static const az_span iot_hub_properties_reported = AZ_SPAN_LITERAL_FROM_STR("reported"); +static const az_span iot_hub_properties_desired = AZ_SPAN_LITERAL_FROM_STR("desired"); +static const az_span iot_hub_properties_desired_version = AZ_SPAN_LITERAL_FROM_STR("$version"); +static const az_span properties_response_value_name = AZ_SPAN_LITERAL_FROM_STR("value"); +static const az_span properties_ack_code_name = AZ_SPAN_LITERAL_FROM_STR("ac"); +static const az_span properties_ack_version_name = AZ_SPAN_LITERAL_FROM_STR("av"); +static const az_span properties_ack_description_name = AZ_SPAN_LITERAL_FROM_STR("ad"); + +static const az_span component_properties_label_name = AZ_SPAN_LITERAL_FROM_STR("__t"); +static const az_span component_properties_label_value = AZ_SPAN_LITERAL_FROM_STR("c"); + +AZ_NODISCARD az_result az_iot_hub_client_properties_get_reported_publish_topic( + az_iot_hub_client const* client, + az_span request_id, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length) +{ + return az_iot_hub_client_twin_patch_get_publish_topic( + client, request_id, mqtt_topic, mqtt_topic_size, out_mqtt_topic_length); +} + +AZ_NODISCARD az_result az_iot_hub_client_properties_document_get_publish_topic( + az_iot_hub_client const* client, + az_span request_id, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length) +{ + return az_iot_hub_client_twin_document_get_publish_topic( + client, request_id, mqtt_topic, mqtt_topic_size, out_mqtt_topic_length); +} + +AZ_NODISCARD az_result az_iot_hub_client_properties_parse_received_topic( + az_iot_hub_client const* client, + az_span received_topic, + az_iot_hub_client_properties_message* out_message) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_VALID_SPAN(received_topic, 1, false); + _az_PRECONDITION_NOT_NULL(out_message); + + az_iot_hub_client_twin_response hub_twin_response; + _az_RETURN_IF_FAILED( + az_iot_hub_client_twin_parse_received_topic(client, received_topic, &hub_twin_response)); + + out_message->request_id = hub_twin_response.request_id; + out_message->message_type + = (az_iot_hub_client_properties_message_type)hub_twin_response.response_type; + out_message->status = hub_twin_response.status; + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_hub_client_properties_writer_begin_component( + az_iot_hub_client const* client, + az_json_writer* ref_json_writer, + az_span component_name) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(ref_json_writer); + _az_PRECONDITION_VALID_SPAN(component_name, 1, false); + + (void)client; + + _az_RETURN_IF_FAILED(az_json_writer_append_property_name(ref_json_writer, component_name)); + _az_RETURN_IF_FAILED(az_json_writer_append_begin_object(ref_json_writer)); + _az_RETURN_IF_FAILED( + az_json_writer_append_property_name(ref_json_writer, component_properties_label_name)); + _az_RETURN_IF_FAILED( + az_json_writer_append_string(ref_json_writer, component_properties_label_value)); + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_hub_client_properties_writer_end_component( + az_iot_hub_client const* client, + az_json_writer* ref_json_writer) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(ref_json_writer); + + (void)client; + + return az_json_writer_append_end_object(ref_json_writer); +} + +AZ_NODISCARD az_result az_iot_hub_client_properties_writer_begin_response_status( + az_iot_hub_client const* client, + az_json_writer* ref_json_writer, + az_span property_name, + int32_t status_code, + int32_t version, + az_span description) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(ref_json_writer); + _az_PRECONDITION_VALID_SPAN(property_name, 1, false); + + (void)client; + + _az_RETURN_IF_FAILED(az_json_writer_append_property_name(ref_json_writer, property_name)); + _az_RETURN_IF_FAILED(az_json_writer_append_begin_object(ref_json_writer)); + _az_RETURN_IF_FAILED( + az_json_writer_append_property_name(ref_json_writer, properties_ack_code_name)); + _az_RETURN_IF_FAILED(az_json_writer_append_int32(ref_json_writer, status_code)); + _az_RETURN_IF_FAILED( + az_json_writer_append_property_name(ref_json_writer, properties_ack_version_name)); + _az_RETURN_IF_FAILED(az_json_writer_append_int32(ref_json_writer, version)); + + if (az_span_size(description) != 0) + { + _az_RETURN_IF_FAILED( + az_json_writer_append_property_name(ref_json_writer, properties_ack_description_name)); + _az_RETURN_IF_FAILED(az_json_writer_append_string(ref_json_writer, description)); + } + + _az_RETURN_IF_FAILED( + az_json_writer_append_property_name(ref_json_writer, properties_response_value_name)); + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_hub_client_properties_writer_end_response_status( + az_iot_hub_client const* client, + az_json_writer* ref_json_writer) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(ref_json_writer); + + (void)client; + + return az_json_writer_append_end_object(ref_json_writer); +} + +// Move reader to the value of property name +static az_result json_child_token_move(az_json_reader* ref_jr, az_span property_name) +{ + do + { + if ((ref_jr->token.kind == AZ_JSON_TOKEN_PROPERTY_NAME) + && az_json_token_is_text_equal(&(ref_jr->token), property_name)) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_jr)); + + return AZ_OK; + } + else if (ref_jr->token.kind == AZ_JSON_TOKEN_BEGIN_OBJECT) + { + if (az_result_failed(az_json_reader_skip_children(ref_jr))) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + } + else if (ref_jr->token.kind == AZ_JSON_TOKEN_END_OBJECT) + { + return AZ_ERROR_ITEM_NOT_FOUND; + } + } while (az_result_succeeded(az_json_reader_next_token(ref_jr))); + + return AZ_ERROR_ITEM_NOT_FOUND; +} + +// Check if the component name is in the model. While this is sometimes +// indicated in the twin metadata (via "__t":"c" as a child), this metadata will NOT +// be specified during a TWIN PATCH operation. Hence we cannot rely on it +// being present. We instead use the application provided component_name list. +static bool is_component_in_model( + az_iot_hub_client const* client, + az_json_token const* component_name, + az_span* out_component_name) +{ + int32_t index = 0; + + while (index < client->_internal.options.component_names_length) + { + if (az_json_token_is_text_equal( + component_name, client->_internal.options.component_names[index])) + { + *out_component_name = client->_internal.options.component_names[index]; + return true; + } + + index++; + } + + return false; +} + +AZ_NODISCARD az_result az_iot_hub_client_properties_get_properties_version( + az_iot_hub_client const* client, + az_json_reader* ref_json_reader, + az_iot_hub_client_properties_message_type message_type, + int32_t* out_version) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(ref_json_reader); + _az_PRECONDITION( + (message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_WRITABLE_UPDATED) + || (message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE)); + _az_PRECONDITION_NOT_NULL(out_version); + + (void)client; + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + if (ref_json_reader->token.kind != AZ_JSON_TOKEN_BEGIN_OBJECT) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + if (message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE) + { + _az_RETURN_IF_FAILED(json_child_token_move(ref_json_reader, iot_hub_properties_desired)); + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + } + + _az_RETURN_IF_FAILED(json_child_token_move(ref_json_reader, iot_hub_properties_desired_version)); + _az_RETURN_IF_FAILED(az_json_token_get_int32(&ref_json_reader->token, out_version)); + + return AZ_OK; +} + +// process_first_move_if_needed performs initial setup when beginning to parse +// the JSON document. It sets the next read token to the appropriate +// location based on whether we have a full twin or a patch and what property_type +// the application requested. +// Returns AZ_OK if this is NOT the first read of the document. +static az_result process_first_move_if_needed( + az_json_reader* jr, + az_iot_hub_client_properties_message_type message_type, + az_iot_hub_client_property_type property_type) + +{ + if (jr->current_depth == 0) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(jr)); + + if (jr->token.kind != AZ_JSON_TOKEN_BEGIN_OBJECT) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + _az_RETURN_IF_FAILED(az_json_reader_next_token(jr)); + + if (message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE) + { + const az_span property_to_query + = (property_type == AZ_IOT_HUB_CLIENT_PROPERTY_REPORTED_FROM_DEVICE) + ? iot_hub_properties_reported + : iot_hub_properties_desired; + _az_RETURN_IF_FAILED(json_child_token_move(jr, property_to_query)); + _az_RETURN_IF_FAILED(az_json_reader_next_token(jr)); + } + return AZ_OK; + } + else + { + // Not the first move so continue + return AZ_OK; + } +} + +// The underlying twin has various metadata embedded in the JSON. +// This metadata should not be passed back to the caller +// but should instead be silently ignored/skipped. +static az_result skip_metadata_if_needed( + az_json_reader* jr, + az_iot_hub_client_properties_message_type message_type) +{ + while (true) + { + // Within the "root" or "component name" section + if ((message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_WRITABLE_UPDATED + && jr->current_depth == 1) + || (message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE + && jr->current_depth == 2)) + { + if ((az_json_token_is_text_equal(&jr->token, iot_hub_properties_desired_version))) + { + // Skip version property name and property value + _az_RETURN_IF_FAILED(az_json_reader_next_token(jr)); + _az_RETURN_IF_FAILED(az_json_reader_next_token(jr)); + + continue; + } + else + { + return AZ_OK; + } + } + // Within the property value section + else if ( + (message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_WRITABLE_UPDATED + && jr->current_depth == 2) + || (message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE + && jr->current_depth == 3)) + { + if (az_json_token_is_text_equal(&jr->token, component_properties_label_name)) + { + // Skip label property name and property value + _az_RETURN_IF_FAILED(az_json_reader_next_token(jr)); + _az_RETURN_IF_FAILED(az_json_reader_next_token(jr)); + + continue; + } + else + { + return AZ_OK; + } + } + else + { + return AZ_OK; + } + } +} + +// verify_valid_json_position makes sure that the az_json_reader +// is in a good state. Applications modify the az_json_reader as they +// traverse properties and a poorly written application could leave +// it in an invalid state. +static az_result verify_valid_json_position( + az_json_reader* jr, + az_iot_hub_client_properties_message_type message_type, + az_span component_name) +{ + // Not on a property name or end of object + if (jr->current_depth != 0 + && (jr->token.kind != AZ_JSON_TOKEN_PROPERTY_NAME + && jr->token.kind != AZ_JSON_TOKEN_END_OBJECT)) + { + return AZ_ERROR_JSON_INVALID_STATE; + } + + // Component property - In user property value object + if ((message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_WRITABLE_UPDATED + && jr->current_depth > 2) + || (message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE + && jr->current_depth > 3)) + { + return AZ_ERROR_JSON_INVALID_STATE; + } + + // Non-component property - In user property value object + if ((az_span_size(component_name) == 0) + && ((message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_WRITABLE_UPDATED + && jr->current_depth > 1) + || (message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE + && jr->current_depth > 2))) + { + return AZ_ERROR_JSON_INVALID_STATE; + } + + return AZ_OK; +} + +/* +Assuming a JSON of either the below types + +AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_WRITABLE_UPDATED: + +{ + //ROOT COMPONENT or COMPONENT NAME section + "component_one": { + //PROPERTY VALUE section + "prop_one": 1, + "prop_two": "string" + }, + "component_two": { + "prop_three": 45, + "prop_four": "string" + }, + "not_component": 42, + "$version": 5 +} + +AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE: + +{ + "desired": { + //ROOT COMPONENT or COMPONENT NAME section + "component_one": { + //PROPERTY VALUE section + "prop_one": 1, + "prop_two": "string" + }, + "component_two": { + "prop_three": 45, + "prop_four": "string" + }, + "not_component": 42, + "$version": 5 + }, + "reported": { + "manufacturer": "Sample-Manufacturer", + "model": "pnp-sample-Model-123", + "swVersion": "1.0.0.0", + "osName": "Contoso" + } +} + +*/ +AZ_NODISCARD az_result az_iot_hub_client_properties_get_next_component_property( + az_iot_hub_client const* client, + az_json_reader* ref_json_reader, + az_iot_hub_client_properties_message_type message_type, + az_iot_hub_client_property_type property_type, + az_span* out_component_name) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(ref_json_reader); + _az_PRECONDITION_NOT_NULL(out_component_name); + _az_PRECONDITION( + (message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_WRITABLE_UPDATED) + || (message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE)); + _az_PRECONDITION( + (property_type == AZ_IOT_HUB_CLIENT_PROPERTY_REPORTED_FROM_DEVICE) + || (property_type == AZ_IOT_HUB_CLIENT_PROPERTY_WRITABLE)); + _az_PRECONDITION( + (property_type == AZ_IOT_HUB_CLIENT_PROPERTY_WRITABLE) + || ((property_type == AZ_IOT_HUB_CLIENT_PROPERTY_REPORTED_FROM_DEVICE) + && (message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE))); + + _az_RETURN_IF_FAILED( + verify_valid_json_position(ref_json_reader, message_type, *out_component_name)); + _az_RETURN_IF_FAILED(process_first_move_if_needed(ref_json_reader, message_type, property_type)); + + while (true) + { + _az_RETURN_IF_FAILED(skip_metadata_if_needed(ref_json_reader, message_type)); + + if (ref_json_reader->token.kind == AZ_JSON_TOKEN_END_OBJECT) + { + // We've read all the children of the current object we're traversing. + if ((message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_WRITABLE_UPDATED + && ref_json_reader->current_depth == 0) + || (message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE + && ref_json_reader->current_depth == 1)) + { + // We've read the last child the root of the JSON tree we're traversing. We're done. + return AZ_ERROR_IOT_END_OF_PROPERTIES; + } + + // There are additional tokens to read. Continue. + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + continue; + } + + break; + } + + if ((message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_WRITABLE_UPDATED + && ref_json_reader->current_depth == 1) + || (message_type == AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE + && ref_json_reader->current_depth == 2)) + { + // Retrieve the next property/component pair. + if (is_component_in_model(client, &ref_json_reader->token, out_component_name)) + { + // Properties that are children of components are simply modeled as JSON children + // in the underlying twin. Traverse into the object. + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + + if (ref_json_reader->token.kind != AZ_JSON_TOKEN_BEGIN_OBJECT) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + _az_RETURN_IF_FAILED(skip_metadata_if_needed(ref_json_reader, message_type)); + } + else + { + // The current property is not the child of a component. + *out_component_name = AZ_SPAN_EMPTY; + } + } + + return AZ_OK; +} diff --git a/src/az_iot_hub_client_properties.h b/src/az_iot_hub_client_properties.h new file mode 100644 index 00000000..60c9a93e --- /dev/null +++ b/src/az_iot_hub_client_properties.h @@ -0,0 +1,308 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Definition for the IoT Plug and Play properties writer and parsing routines. + */ + +#ifndef _az_IOT_HUB_CLIENT_PROPERTIES_H +#define _az_IOT_HUB_CLIENT_PROPERTIES_H + +#include +#include + +#include +#include +#include + +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief Append the necessary characters to a reported properties JSON payload belonging to a + * component. + * + * The payload will be of the form: + * + * @code + * "": { + * "__t": "c", + * "temperature": 23 + * } + * @endcode + * + * @note This API only writes the metadata for a component's properties. The + * application itself must specify the payload contents between calls + * to this API and az_iot_hub_client_properties_writer_end_component() using + * \p ref_json_writer to specify the JSON payload. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in,out] ref_json_writer The #az_json_writer to append the necessary characters for an IoT + * Plug and Play component. + * @param[in] component_name The component name associated with the reported properties. + * + * @pre \p client must not be `NULL`. + * @pre \p ref_json_writer must not be `NULL`. + * @pre \p component_name must be a valid, non-empty #az_span. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The JSON payload was prefixed successfully. + */ +AZ_NODISCARD az_result az_iot_hub_client_properties_writer_begin_component( + az_iot_hub_client const* client, + az_json_writer* ref_json_writer, + az_span component_name); + +/** + * @brief Append the necessary characters to end a reported properties JSON payload belonging to a + * component. + * + * @note This API should be used in conjunction with + * az_iot_hub_client_properties_writer_begin_component(). + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in,out] ref_json_writer The #az_json_writer to append the necessary characters for an IoT + * Plug and Play component. + * + * @pre \p client must not be `NULL`. + * @pre \p ref_json_writer must not be `NULL`. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The JSON payload was suffixed successfully. + */ +AZ_NODISCARD az_result az_iot_hub_client_properties_writer_end_component( + az_iot_hub_client const* client, + az_json_writer* ref_json_writer); + +/** + * @brief Begin a property response to a writable property request from the service. + * + * This API should be used in response to an incoming writable properties. More details can be + * found here: + * + * https://docs.microsoft.com/azure/iot-pnp/concepts-convention#writable-properties + * + * The payload will be of the form: + * + * **Without component** + * @code + * { + * "":{ + * "ac": , + * "av": , + * "ad": "", + * "value": + * } + * } + * @endcode + * + * **With component** + * @code + * { + * "": { + * "__t": "c", + * "": { + * "ac": , + * "av": , + * "ad": "", + * "value": + * } + * } + * } + * @endcode + * + * To send a status for properties belonging to a component, first call the + * az_iot_hub_client_properties_writer_begin_component() API to prefix the payload with the + * necessary identification. The API call flow would look like the following with the listed JSON + * payload being generated. + * + * @code + * az_iot_hub_client_properties_writer_begin_component() + * az_iot_hub_client_properties_writer_begin_response_status() + * // Append user value here () using ref_json_writer directly. + * az_iot_hub_client_properties_writer_end_response_status() + * az_iot_hub_client_properties_writer_end_component() + * @endcode + * + * @note This API only writes the metadata for the properties response. The + * application itself must specify the payload contents between calls + * to this API and az_iot_hub_client_properties_writer_end_response_status() using + * \p ref_json_writer to specify the JSON payload. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in,out] ref_json_writer The initialized #az_json_writer to append data to. + * @param[in] property_name The name of the property to write a response payload for. + * @param[in] status_code The HTTP-like status code to respond with. See #az_iot_status for + * possible supported values. + * @param[in] version The version of the property the application is acknowledging. + * This can be retrieved from the service request by + * calling az_iot_hub_client_properties_get_properties_version. + * @param[in] description An optional description detailing the context or any details about + * the acknowledgement. This can be #AZ_SPAN_EMPTY. + * + * @pre \p client must not be `NULL`. + * @pre \p ref_json_writer must not be `NULL`. + * @pre \p property_name must be a valid, non-empty #az_span. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The JSON payload was prefixed successfully. + */ +AZ_NODISCARD az_result az_iot_hub_client_properties_writer_begin_response_status( + az_iot_hub_client const* client, + az_json_writer* ref_json_writer, + az_span property_name, + int32_t status_code, + int32_t version, + az_span description); + +/** + * @brief End a properties response payload with confirmation status. + * + * @note This API should be used in conjunction with + * az_iot_hub_client_properties_writer_begin_response_status(). + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in,out] ref_json_writer The initialized #az_json_writer to append data to. + * + * @pre \p client must not be `NULL`. + * @pre \p ref_json_writer must not be `NULL`. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The JSON payload was suffixed successfully. + */ +AZ_NODISCARD az_result az_iot_hub_client_properties_writer_end_response_status( + az_iot_hub_client const* client, + az_json_writer* ref_json_writer); + +/** + * @brief Read the IoT Plug and Play property version. + * + * @warning This modifies the state of the json reader. To then use the same json reader + * with az_iot_hub_client_properties_get_next_component_property(), you must call + * az_json_reader_init() again after this call and before the call to + * az_iot_hub_client_properties_get_next_component_property() or make an additional copy before + * calling this API. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in,out] ref_json_reader The pointer to the #az_json_reader used to parse through the JSON + * payload. + * @param[in] message_type The #az_iot_hub_client_properties_message_type representing the message + * type associated with the payload. + * @param[out] out_version The numeric version of the properties in the JSON payload. + * + * @pre \p client must not be `NULL`. + * @pre \p ref_json_reader must not be `NULL`. + * @pre \p message_type must be `AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_WRITABLE_UPDATED` or + * `AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE`. + * @pre \p out_version must not be `NULL`. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK If the function returned a valid version. + */ +AZ_NODISCARD az_result az_iot_hub_client_properties_get_properties_version( + az_iot_hub_client const* client, + az_json_reader* ref_json_reader, + az_iot_hub_client_properties_message_type message_type, + int32_t* out_version); + +/** + * @brief Property type + * + */ +typedef enum +{ + /** @brief Property was originally reported from the device. */ + AZ_IOT_HUB_CLIENT_PROPERTY_REPORTED_FROM_DEVICE, + /** @brief Property was received from the service. */ + AZ_IOT_HUB_CLIENT_PROPERTY_WRITABLE +} az_iot_hub_client_property_type; + +/** + * @brief Iteratively read the IoT Plug and Play component properties. + * + * Note that between calls, the #az_span pointed to by \p out_component_name shall not be modified, + * only checked and compared. Internally, the #az_span is only changed if the component name changes + * in the JSON document and is not necessarily set every invocation of the function. + * + * On success, the `ref_json_reader` will be set on a valid property name. After checking the + * property name, the reader can be advanced to the property value by calling + * az_json_reader_next_token(). Note that on the subsequent call to this API, it is expected that + * the json reader will be placed AFTER the read property name and value. That means that after + * reading the property value (including single values or complex objects), the user must call + * az_json_reader_next_token(). + * + * Below is a code snippet which you can use as a starting point: + * + * @code + * + * while (az_result_succeeded(az_iot_hub_client_properties_get_next_component_property( + * &hub_client, &jr, message_type, AZ_IOT_HUB_CLIENT_PROPERTY_WRITABLE, &component_name))) + * { + * // Check if property is of interest (substitute user_property for your own property name) + * if (az_json_token_is_text_equal(&jr.token, user_property)) + * { + * az_json_reader_next_token(&jr); + * + * // Get the property value here + * // Example: az_json_token_get_int32(&jr.token, &user_int); + * + * // Skip to next property value + * az_json_reader_next_token(&jr); + * } + * else + * { + * // The JSON reader must be advanced regardless of whether the property + * // is of interest or not. + * az_json_reader_next_token(&jr); + * + * // Skip children in case the property value is an object + * az_json_reader_skip_children(&jr); + * az_json_reader_next_token(&jr); + * } + * } + * + * @endcode + * + * @warning If you need to retrieve more than one \p property_type, you should first complete the + * scan of all components for the first property type (until the API returns + * #AZ_ERROR_IOT_END_OF_PROPERTIES). Then you must call az_json_reader_init() again after this call + * and before the next call to az_iot_hub_client_properties_get_next_component_property with the + * different \p property_type. + * + * @param[in] client The #az_iot_hub_client to use for this call. + * @param[in,out] ref_json_reader The #az_json_reader to parse through. The ownership of iterating + * through this json reader is shared between the user and this API. + * @param[in] message_type The #az_iot_hub_client_properties_message_type representing the message + * type associated with the payload. + * @param[in] property_type The #az_iot_hub_client_property_type to scan for. + * @param[out] out_component_name The #az_span* representing the value of the component. + * + * @pre \p client must not be `NULL`. + * @pre \p ref_json_reader must not be `NULL`. + * @pre \p out_component_name must not be `NULL`. It must point to an #az_span instance. + * @pre \p message_type must be `AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_WRITABLE_UPDATED` or + * `AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE`. + * @pre \p property_type must be `AZ_IOT_HUB_CLIENT_PROPERTY_REPORTED_FROM_DEVICE` or + * `AZ_IOT_HUB_CLIENT_PROPERTY_WRITABLE`. + * @pre \p If `AZ_IOT_HUB_CLIENT_PROPERTY_REPORTED_FROM_DEVICE` is specified in \p property_type, + * then \p message_type must be `AZ_IOT_HUB_CLIENT_PROPERTIES_MESSAGE_TYPE_GET_RESPONSE`. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK If the function returned a valid #az_json_reader pointing to the property name and + * the #az_span with a component name. + * @retval #AZ_ERROR_JSON_INVALID_STATE If the json reader is passed in at an unexpected location. + * @retval #AZ_ERROR_IOT_END_OF_PROPERTIES If there are no more properties left for the component. + */ +AZ_NODISCARD az_result az_iot_hub_client_properties_get_next_component_property( + az_iot_hub_client const* client, + az_json_reader* ref_json_reader, + az_iot_hub_client_properties_message_type message_type, + az_iot_hub_client_property_type property_type, + az_span* out_component_name); + +#include <_az_cfg_suffix.h> + +#endif //_az_IOT_HUB_CLIENT_PROPERTIES_H diff --git a/src/az_iot_hub_client_sas.c b/src/az_iot_hub_client_sas.c new file mode 100644 index 00000000..4ddf8622 --- /dev/null +++ b/src/az_iot_hub_client_sas.c @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include <_az_cfg.h> + +#define LF '\n' +#define AMPERSAND '&' +#define EQUAL_SIGN '=' +#define STRING_NULL_TERMINATOR '\0' +#define SCOPE_DEVICES_STRING "%2Fdevices%2F" +#define SCOPE_MODULES_STRING "%2Fmodules%2F" +#define SAS_TOKEN_SR "SharedAccessSignature sr" +#define SAS_TOKEN_SE "se" +#define SAS_TOKEN_SIG "sig" +#define SAS_TOKEN_SKN "skn" + +static const az_span devices_string = AZ_SPAN_LITERAL_FROM_STR(SCOPE_DEVICES_STRING); +static const az_span modules_string = AZ_SPAN_LITERAL_FROM_STR(SCOPE_MODULES_STRING); +static const az_span skn_string = AZ_SPAN_LITERAL_FROM_STR(SAS_TOKEN_SKN); +static const az_span sr_string = AZ_SPAN_LITERAL_FROM_STR(SAS_TOKEN_SR); +static const az_span sig_string = AZ_SPAN_LITERAL_FROM_STR(SAS_TOKEN_SIG); +static const az_span se_string = AZ_SPAN_LITERAL_FROM_STR(SAS_TOKEN_SE); + +AZ_NODISCARD az_result az_iot_hub_client_sas_get_signature( + az_iot_hub_client const* client, + uint64_t token_expiration_epoch_time, + az_span signature, + az_span* out_signature) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION(token_expiration_epoch_time > 0); + _az_PRECONDITION_VALID_SPAN(signature, 1, false); + _az_PRECONDITION_NOT_NULL(out_signature); + + az_span remainder = signature; + int32_t signature_size = az_span_size(signature); + + _az_RETURN_IF_FAILED( + _az_span_copy_url_encode(remainder, client->_internal.iot_hub_hostname, &remainder)); + + _az_RETURN_IF_NOT_ENOUGH_SIZE(remainder, az_span_size(devices_string)); + remainder = az_span_copy(remainder, devices_string); + + _az_RETURN_IF_FAILED( + _az_span_copy_url_encode(remainder, client->_internal.device_id, &remainder)); + + if (az_span_size(client->_internal.options.module_id) > 0) + { + _az_RETURN_IF_NOT_ENOUGH_SIZE(remainder, az_span_size(modules_string)); + remainder = az_span_copy(remainder, modules_string); + + _az_RETURN_IF_FAILED( + _az_span_copy_url_encode(remainder, client->_internal.options.module_id, &remainder)); + } + + _az_RETURN_IF_NOT_ENOUGH_SIZE( + remainder, + 1 + // LF + _az_iot_u64toa_size(token_expiration_epoch_time)); + + remainder = az_span_copy_u8(remainder, LF); + + _az_RETURN_IF_FAILED(az_span_u64toa(remainder, token_expiration_epoch_time, &remainder)); + + *out_signature = az_span_slice(signature, 0, signature_size - az_span_size(remainder)); + _az_LOG_WRITE(AZ_LOG_IOT_SAS_TOKEN, *out_signature); + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_hub_client_sas_get_password( + az_iot_hub_client const* client, + uint64_t token_expiration_epoch_time, + az_span base64_hmac_sha256_signature, + az_span key_name, + char* mqtt_password, + size_t mqtt_password_size, + size_t* out_mqtt_password_length) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_VALID_SPAN(base64_hmac_sha256_signature, 1, false); + _az_PRECONDITION(token_expiration_epoch_time > 0); + _az_PRECONDITION_NOT_NULL(mqtt_password); + _az_PRECONDITION(mqtt_password_size > 0); + + // Concatenates: "SharedAccessSignature sr=" scope "&sig=" sig "&se=" expiration_time_secs + // plus, if key_name size > 0, "&skn=" key_name + + az_span mqtt_password_span = az_span_create((uint8_t*)mqtt_password, (int32_t)mqtt_password_size); + + // SharedAccessSignature + _az_RETURN_IF_NOT_ENOUGH_SIZE(mqtt_password_span, az_span_size(sr_string) + 1 /* EQUAL_SIGN */); + mqtt_password_span = az_span_copy(mqtt_password_span, sr_string); + mqtt_password_span = az_span_copy_u8(mqtt_password_span, EQUAL_SIGN); + + _az_RETURN_IF_FAILED(_az_span_copy_url_encode( + mqtt_password_span, client->_internal.iot_hub_hostname, &mqtt_password_span)); + + // Device ID + _az_RETURN_IF_NOT_ENOUGH_SIZE(mqtt_password_span, az_span_size(devices_string)); + mqtt_password_span = az_span_copy(mqtt_password_span, devices_string); + + _az_RETURN_IF_FAILED(_az_span_copy_url_encode( + mqtt_password_span, client->_internal.device_id, &mqtt_password_span)); + + // Module ID + if (az_span_size(client->_internal.options.module_id) > 0) + { + _az_RETURN_IF_NOT_ENOUGH_SIZE(mqtt_password_span, az_span_size(modules_string)); + mqtt_password_span = az_span_copy(mqtt_password_span, modules_string); + + _az_RETURN_IF_FAILED(_az_span_copy_url_encode( + mqtt_password_span, client->_internal.options.module_id, &mqtt_password_span)); + } + + // Signature + _az_RETURN_IF_NOT_ENOUGH_SIZE( + mqtt_password_span, 1 /* AMPERSAND */ + az_span_size(sig_string) + 1 /* EQUAL_SIGN */); + + mqtt_password_span = az_span_copy_u8(mqtt_password_span, AMPERSAND); + mqtt_password_span = az_span_copy(mqtt_password_span, sig_string); + mqtt_password_span = az_span_copy_u8(mqtt_password_span, EQUAL_SIGN); + + _az_RETURN_IF_FAILED(_az_span_copy_url_encode( + mqtt_password_span, base64_hmac_sha256_signature, &mqtt_password_span)); + + // Expiration + _az_RETURN_IF_NOT_ENOUGH_SIZE( + mqtt_password_span, 1 /* AMPERSAND */ + az_span_size(se_string) + 1 /* EQUAL_SIGN */); + mqtt_password_span = az_span_copy_u8(mqtt_password_span, AMPERSAND); + mqtt_password_span = az_span_copy(mqtt_password_span, se_string); + mqtt_password_span = az_span_copy_u8(mqtt_password_span, EQUAL_SIGN); + + _az_RETURN_IF_FAILED( + az_span_u64toa(mqtt_password_span, token_expiration_epoch_time, &mqtt_password_span)); + + if (az_span_size(key_name) > 0) + { + // Key Name + _az_RETURN_IF_NOT_ENOUGH_SIZE( + mqtt_password_span, + 1 /* AMPERSAND */ + az_span_size(skn_string) + 1 /* EQUAL_SIGN */ + az_span_size(key_name)); + mqtt_password_span = az_span_copy_u8(mqtt_password_span, AMPERSAND); + mqtt_password_span = az_span_copy(mqtt_password_span, skn_string); + mqtt_password_span = az_span_copy_u8(mqtt_password_span, EQUAL_SIGN); + mqtt_password_span = az_span_copy(mqtt_password_span, key_name); + } + + _az_RETURN_IF_NOT_ENOUGH_SIZE(mqtt_password_span, 1 /* NULL TERMINATOR */); + + mqtt_password_span = az_span_copy_u8(mqtt_password_span, STRING_NULL_TERMINATOR); + + if (out_mqtt_password_length != NULL) + { + *out_mqtt_password_length + = mqtt_password_size - (size_t)az_span_size(mqtt_password_span) - 1 /* NULL TERMINATOR */; + } + + return AZ_OK; +} diff --git a/src/az_iot_hub_client_telemetry.c b/src/az_iot_hub_client_telemetry.c new file mode 100644 index 00000000..d3adb54c --- /dev/null +++ b/src/az_iot_hub_client_telemetry.c @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include +#include + +#include + +#include <_az_cfg.h> + +static const uint8_t null_terminator = '\0'; +static const az_span telemetry_topic_prefix = AZ_SPAN_LITERAL_FROM_STR("devices/"); +static const az_span telemetry_topic_modules_mid = AZ_SPAN_LITERAL_FROM_STR("/modules/"); +static const az_span telemetry_topic_suffix = AZ_SPAN_LITERAL_FROM_STR("/messages/events/"); + +AZ_NODISCARD az_result az_iot_hub_client_telemetry_get_publish_topic( + az_iot_hub_client const* client, + az_iot_message_properties const* properties, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(mqtt_topic); + _az_PRECONDITION(mqtt_topic_size > 0); + + const az_span* const module_id = &(client->_internal.options.module_id); + + az_span mqtt_topic_span = az_span_create((uint8_t*)mqtt_topic, (int32_t)mqtt_topic_size); + int32_t required_length = az_span_size(telemetry_topic_prefix) + + az_span_size(client->_internal.device_id) + az_span_size(telemetry_topic_suffix); + int32_t module_id_length = az_span_size(*module_id); + if (module_id_length > 0) + { + required_length += az_span_size(telemetry_topic_modules_mid) + module_id_length; + } + if (properties != NULL) + { + required_length += properties->_internal.properties_written; + } + + _az_RETURN_IF_NOT_ENOUGH_SIZE( + mqtt_topic_span, required_length + (int32_t)sizeof(null_terminator)); + + az_span remainder = az_span_copy(mqtt_topic_span, telemetry_topic_prefix); + remainder = az_span_copy(remainder, client->_internal.device_id); + + if (module_id_length > 0) + { + remainder = az_span_copy(remainder, telemetry_topic_modules_mid); + remainder = az_span_copy(remainder, *module_id); + } + + remainder = az_span_copy(remainder, telemetry_topic_suffix); + + if (properties != NULL) + { + remainder = az_span_copy( + remainder, + az_span_slice( + properties->_internal.properties_buffer, 0, properties->_internal.properties_written)); + } + + az_span_copy_u8(remainder, null_terminator); + + if (out_mqtt_topic_length) + { + *out_mqtt_topic_length = (size_t)required_length; + } + + return AZ_OK; +} diff --git a/src/az_iot_hub_client_twin.c b/src/az_iot_hub_client_twin.c new file mode 100644 index 00000000..49415a6a --- /dev/null +++ b/src/az_iot_hub_client_twin.c @@ -0,0 +1,221 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include <_az_cfg.h> + +static const uint8_t null_terminator = '\0'; +static const uint8_t az_iot_hub_client_twin_question = '?'; +static const uint8_t az_iot_hub_client_twin_equals = '='; +static const az_span az_iot_hub_client_request_id_span = AZ_SPAN_LITERAL_FROM_STR("$rid"); +static const az_span az_iot_hub_twin_topic_prefix = AZ_SPAN_LITERAL_FROM_STR("$iothub/twin/"); +static const az_span az_iot_hub_twin_response_sub_topic = AZ_SPAN_LITERAL_FROM_STR("res/"); +static const az_span az_iot_hub_twin_get_pub_topic = AZ_SPAN_LITERAL_FROM_STR("GET/"); +static const az_span az_iot_hub_twin_version_prop = AZ_SPAN_LITERAL_FROM_STR("$version"); +static const az_span az_iot_hub_twin_patch_pub_topic + = AZ_SPAN_LITERAL_FROM_STR("PATCH/properties/reported/"); +static const az_span az_iot_hub_twin_patch_sub_topic + = AZ_SPAN_LITERAL_FROM_STR("PATCH/properties/desired/"); + +AZ_NODISCARD az_result az_iot_hub_client_twin_document_get_publish_topic( + az_iot_hub_client const* client, + az_span request_id, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_VALID_SPAN(client->_internal.iot_hub_hostname, 1, false); + _az_PRECONDITION_VALID_SPAN(request_id, 1, false); + _az_PRECONDITION_NOT_NULL(mqtt_topic); + _az_PRECONDITION(mqtt_topic_size > 0); + (void)client; + + az_span mqtt_topic_span = az_span_create((uint8_t*)mqtt_topic, (int32_t)mqtt_topic_size); + int32_t required_length = az_span_size(az_iot_hub_twin_topic_prefix) + + az_span_size(az_iot_hub_twin_get_pub_topic) + + (int32_t)sizeof(az_iot_hub_client_twin_question) + + az_span_size(az_iot_hub_client_request_id_span) + + (int32_t)sizeof(az_iot_hub_client_twin_equals) + az_span_size(request_id); + + _az_RETURN_IF_NOT_ENOUGH_SIZE( + mqtt_topic_span, required_length + (int32_t)sizeof(null_terminator)); + + az_span remainder = az_span_copy(mqtt_topic_span, az_iot_hub_twin_topic_prefix); + remainder = az_span_copy(remainder, az_iot_hub_twin_get_pub_topic); + remainder = az_span_copy_u8(remainder, az_iot_hub_client_twin_question); + remainder = az_span_copy(remainder, az_iot_hub_client_request_id_span); + remainder = az_span_copy_u8(remainder, az_iot_hub_client_twin_equals); + remainder = az_span_copy(remainder, request_id); + az_span_copy_u8(remainder, null_terminator); + + if (out_mqtt_topic_length) + { + *out_mqtt_topic_length = (size_t)required_length; + } + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_hub_client_twin_patch_get_publish_topic( + az_iot_hub_client const* client, + az_span request_id, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_VALID_SPAN(client->_internal.iot_hub_hostname, 1, false); + _az_PRECONDITION_VALID_SPAN(request_id, 1, false); + _az_PRECONDITION_NOT_NULL(mqtt_topic); + _az_PRECONDITION(mqtt_topic_size > 0); + (void)client; + + az_span mqtt_topic_span = az_span_create((uint8_t*)mqtt_topic, (int32_t)mqtt_topic_size); + int32_t required_length = az_span_size(az_iot_hub_twin_topic_prefix) + + az_span_size(az_iot_hub_twin_patch_pub_topic) + + (int32_t)sizeof(az_iot_hub_client_twin_question) + + az_span_size(az_iot_hub_client_request_id_span) + + (int32_t)sizeof(az_iot_hub_client_twin_equals) + az_span_size(request_id); + + _az_RETURN_IF_NOT_ENOUGH_SIZE( + mqtt_topic_span, required_length + (int32_t)sizeof(null_terminator)); + + az_span remainder = az_span_copy(mqtt_topic_span, az_iot_hub_twin_topic_prefix); + remainder = az_span_copy(remainder, az_iot_hub_twin_patch_pub_topic); + remainder = az_span_copy_u8(remainder, az_iot_hub_client_twin_question); + remainder = az_span_copy(remainder, az_iot_hub_client_request_id_span); + remainder = az_span_copy_u8(remainder, az_iot_hub_client_twin_equals); + remainder = az_span_copy(remainder, request_id); + az_span_copy_u8(remainder, null_terminator); + + if (out_mqtt_topic_length) + { + *out_mqtt_topic_length = (size_t)required_length; + } + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_hub_client_twin_parse_received_topic( + az_iot_hub_client const* client, + az_span received_topic, + az_iot_hub_client_twin_response* out_response) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_VALID_SPAN(client->_internal.iot_hub_hostname, 1, false); + _az_PRECONDITION_VALID_SPAN(received_topic, 1, false); + _az_PRECONDITION_NOT_NULL(out_response); + (void)client; + + az_result result = AZ_OK; + + int32_t twin_index = az_span_find(received_topic, az_iot_hub_twin_topic_prefix); + // Check if is related to twin or not + if (twin_index >= 0) + { + _az_LOG_WRITE(AZ_LOG_MQTT_RECEIVED_TOPIC, received_topic); + + int32_t twin_feature_index = -1; + az_span twin_feature_span + = az_span_slice(received_topic, twin_index, az_span_size(received_topic)); + + if ((twin_feature_index = az_span_find(twin_feature_span, az_iot_hub_twin_response_sub_topic)) + >= 0) + { + // Is a res case + int32_t index = 0; + az_span remainder; + az_span status_str = _az_span_token( + az_span_slice( + received_topic, + twin_feature_index + az_span_size(az_iot_hub_twin_response_sub_topic), + az_span_size(received_topic)), + AZ_SPAN_FROM_STR("/"), + &remainder, + &index); + + // Get status and convert to enum + uint32_t status_int = 0; + _az_RETURN_IF_FAILED(az_span_atou32(status_str, &status_int)); + out_response->status = (az_iot_status)status_int; + + if (index == -1) + { + return AZ_ERROR_UNEXPECTED_END; + } + + // Get request id prop value + az_iot_message_properties props; + az_span prop_span = az_span_slice(remainder, 1, az_span_size(remainder)); + _az_RETURN_IF_FAILED( + az_iot_message_properties_init(&props, prop_span, az_span_size(prop_span))); + _az_RETURN_IF_FAILED(az_iot_message_properties_find( + &props, az_iot_hub_client_request_id_span, &out_response->request_id)); + + if (out_response->status >= AZ_IOT_STATUS_BAD_REQUEST) // 400+ + { + // Is an error response + out_response->response_type = AZ_IOT_HUB_CLIENT_TWIN_RESPONSE_TYPE_REQUEST_ERROR; + out_response->version = AZ_SPAN_EMPTY; + } + else if (out_response->status == AZ_IOT_STATUS_NO_CONTENT) // 204 + { + // Is a reported prop response + out_response->response_type = AZ_IOT_HUB_CLIENT_TWIN_RESPONSE_TYPE_REPORTED_PROPERTIES; + _az_RETURN_IF_FAILED(az_iot_message_properties_find( + &props, az_iot_hub_twin_version_prop, &out_response->version)); + } + else // 200 or 202 + { + // Is a twin GET response + out_response->response_type = AZ_IOT_HUB_CLIENT_TWIN_RESPONSE_TYPE_GET; + out_response->version = AZ_SPAN_EMPTY; + } + + result = AZ_OK; + } + else if ( + (twin_feature_index = az_span_find(twin_feature_span, az_iot_hub_twin_patch_sub_topic)) + >= 0) + { + // Is a /PATCH case (desired props) + az_iot_message_properties props; + az_span prop_span = az_span_slice( + received_topic, + twin_feature_index + az_span_size(az_iot_hub_twin_patch_sub_topic) + + (int32_t)sizeof(az_iot_hub_client_twin_question), + az_span_size(received_topic)); + _az_RETURN_IF_FAILED( + az_iot_message_properties_init(&props, prop_span, az_span_size(prop_span))); + _az_RETURN_IF_FAILED(az_iot_message_properties_find( + &props, az_iot_hub_twin_version_prop, &out_response->version)); + + out_response->response_type = AZ_IOT_HUB_CLIENT_TWIN_RESPONSE_TYPE_DESIRED_PROPERTIES; + out_response->request_id = AZ_SPAN_EMPTY; + out_response->status = AZ_IOT_STATUS_OK; + + result = AZ_OK; + } + else + { + result = AZ_ERROR_IOT_TOPIC_NO_MATCH; + } + } + else + { + result = AZ_ERROR_IOT_TOPIC_NO_MATCH; + } + + return result; +} diff --git a/src/az_iot_provisioning_client.c b/src/az_iot_provisioning_client.c new file mode 100644 index 00000000..62f4ac00 --- /dev/null +++ b/src/az_iot_provisioning_client.c @@ -0,0 +1,605 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include <_az_cfg.h> + +static const az_span str_put_iotdps_register + = AZ_SPAN_LITERAL_FROM_STR("PUT/iotdps-register/?$rid=1"); +static const az_span str_get_iotdps_get_operationstatus + = AZ_SPAN_LITERAL_FROM_STR("GET/iotdps-get-operationstatus/?$rid=1&operationId="); + +// From the protocol described in +// https://docs.microsoft.com/azure/iot-dps/iot-dps-mqtt-support#registering-a-device +static const az_span prov_registration_id_label = AZ_SPAN_LITERAL_FROM_STR("registrationId"); +static const az_span prov_payload_label = AZ_SPAN_LITERAL_FROM_STR("payload"); + +// $dps/registrations/res/ +AZ_INLINE az_span _az_iot_provisioning_get_dps_registrations_res() +{ + az_span sub_topic = AZ_SPAN_LITERAL_FROM_STR(AZ_IOT_PROVISIONING_CLIENT_REGISTER_SUBSCRIBE_TOPIC); + + // NOLINTNEXTLINE(readability-magic-numbers, cppcoreguidelines-avoid-magic-numbers) + return az_span_slice(sub_topic, 0, 23); +} + +// /registrations/ +AZ_INLINE az_span _az_iot_provisioning_get_str_registrations() +{ + // NOLINTNEXTLINE(readability-magic-numbers, cppcoreguidelines-avoid-magic-numbers) + return az_span_slice(_az_iot_provisioning_get_dps_registrations_res(), 4, 19); +} + +// $dps/registrations/ +AZ_INLINE az_span _az_iot_provisioning_get_str_dps_registrations() +{ + // NOLINTNEXTLINE(readability-magic-numbers, cppcoreguidelines-avoid-magic-numbers) + return az_span_slice(_az_iot_provisioning_get_dps_registrations_res(), 0, 19); +} + +AZ_NODISCARD az_iot_provisioning_client_options az_iot_provisioning_client_options_default() +{ + return (az_iot_provisioning_client_options){ .user_agent = AZ_SPAN_EMPTY }; +} + +AZ_NODISCARD az_result az_iot_provisioning_client_init( + az_iot_provisioning_client* client, + az_span global_device_hostname, + az_span id_scope, + az_span registration_id, + az_iot_provisioning_client_options const* options) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_VALID_SPAN(global_device_hostname, 1, false); + _az_PRECONDITION_VALID_SPAN(id_scope, 1, false); + _az_PRECONDITION_VALID_SPAN(registration_id, 1, false); + + client->_internal.global_device_endpoint = global_device_hostname; + client->_internal.id_scope = id_scope; + client->_internal.registration_id = registration_id; + + client->_internal.options + = options == NULL ? az_iot_provisioning_client_options_default() : *options; + + return AZ_OK; +} + +// /registrations//api-version= +AZ_NODISCARD az_result az_iot_provisioning_client_get_user_name( + az_iot_provisioning_client const* client, + char* mqtt_user_name, + size_t mqtt_user_name_size, + size_t* out_mqtt_user_name_length) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(mqtt_user_name); + _az_PRECONDITION(mqtt_user_name_size > 0); + + az_span provisioning_service_api_version + = AZ_SPAN_LITERAL_FROM_STR("/api-version=" AZ_IOT_PROVISIONING_SERVICE_VERSION); + az_span user_agent_version_prefix = AZ_SPAN_LITERAL_FROM_STR("&ClientVersion="); + + az_span mqtt_user_name_span + = az_span_create((uint8_t*)mqtt_user_name, (int32_t)mqtt_user_name_size); + + const az_span* const user_agent = &(client->_internal.options.user_agent); + az_span str_registrations = _az_iot_provisioning_get_str_registrations(); + + int32_t required_length = az_span_size(client->_internal.id_scope) + + az_span_size(str_registrations) + az_span_size(client->_internal.registration_id) + + az_span_size(provisioning_service_api_version); + if (az_span_size(*user_agent) > 0) + { + required_length += az_span_size(user_agent_version_prefix) + az_span_size(*user_agent); + } + + _az_RETURN_IF_NOT_ENOUGH_SIZE( + mqtt_user_name_span, required_length + (int32_t)sizeof((uint8_t)'\0')); + + az_span remainder = az_span_copy(mqtt_user_name_span, client->_internal.id_scope); + remainder = az_span_copy(remainder, str_registrations); + remainder = az_span_copy(remainder, client->_internal.registration_id); + remainder = az_span_copy(remainder, provisioning_service_api_version); + + if (az_span_size(*user_agent) > 0) + { + remainder = az_span_copy(remainder, user_agent_version_prefix); + remainder = az_span_copy(remainder, *user_agent); + } + + az_span_copy_u8(remainder, '\0'); + + if (out_mqtt_user_name_length) + { + *out_mqtt_user_name_length = (size_t)required_length; + } + + return AZ_OK; +} + +// +AZ_NODISCARD az_result az_iot_provisioning_client_get_client_id( + az_iot_provisioning_client const* client, + char* mqtt_client_id, + size_t mqtt_client_id_size, + size_t* out_mqtt_client_id_length) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(mqtt_client_id); + _az_PRECONDITION(mqtt_client_id_size > 0); + + az_span mqtt_client_id_span + = az_span_create((uint8_t*)mqtt_client_id, (int32_t)mqtt_client_id_size); + + int32_t required_length = az_span_size(client->_internal.registration_id); + + _az_RETURN_IF_NOT_ENOUGH_SIZE( + mqtt_client_id_span, required_length + (int32_t)sizeof((uint8_t)'\0')); + + az_span remainder = az_span_copy(mqtt_client_id_span, client->_internal.registration_id); + az_span_copy_u8(remainder, '\0'); + + if (out_mqtt_client_id_length) + { + *out_mqtt_client_id_length = (size_t)required_length; + } + + return AZ_OK; +} + +// $dps/registrations/PUT/iotdps-register/?$rid=%s +AZ_NODISCARD az_result az_iot_provisioning_client_register_get_publish_topic( + az_iot_provisioning_client const* client, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length) +{ + (void)client; + + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_VALID_SPAN(client->_internal.global_device_endpoint, 1, false); + _az_PRECONDITION_NOT_NULL(mqtt_topic); + _az_PRECONDITION(mqtt_topic_size > 0); + + az_span mqtt_topic_span = az_span_create((uint8_t*)mqtt_topic, (int32_t)mqtt_topic_size); + az_span str_dps_registrations = _az_iot_provisioning_get_str_dps_registrations(); + + int32_t required_length + = az_span_size(str_dps_registrations) + az_span_size(str_put_iotdps_register); + + _az_RETURN_IF_NOT_ENOUGH_SIZE(mqtt_topic_span, required_length + (int32_t)sizeof((uint8_t)'\0')); + + az_span remainder = az_span_copy(mqtt_topic_span, str_dps_registrations); + remainder = az_span_copy(remainder, str_put_iotdps_register); + az_span_copy_u8(remainder, '\0'); + + if (out_mqtt_topic_length) + { + *out_mqtt_topic_length = (size_t)required_length; + } + + return AZ_OK; +} + +// Topic: $dps/registrations/GET/iotdps-get-operationstatus/?$rid=%s&operationId=%s +AZ_NODISCARD az_result az_iot_provisioning_client_query_status_get_publish_topic( + az_iot_provisioning_client const* client, + az_span operation_id, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length) +{ + (void)client; + + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_VALID_SPAN(client->_internal.global_device_endpoint, 1, false); + _az_PRECONDITION_NOT_NULL(mqtt_topic); + _az_PRECONDITION(mqtt_topic_size > 0); + + _az_PRECONDITION_VALID_SPAN(operation_id, 1, false); + + az_span mqtt_topic_span = az_span_create((uint8_t*)mqtt_topic, (int32_t)mqtt_topic_size); + az_span str_dps_registrations = _az_iot_provisioning_get_str_dps_registrations(); + + int32_t required_length = az_span_size(str_dps_registrations) + + az_span_size(str_get_iotdps_get_operationstatus) + az_span_size(operation_id); + + _az_RETURN_IF_NOT_ENOUGH_SIZE(mqtt_topic_span, required_length + (int32_t)sizeof((uint8_t)'\0')); + + az_span remainder = az_span_copy(mqtt_topic_span, str_dps_registrations); + remainder = az_span_copy(remainder, str_get_iotdps_get_operationstatus); + remainder = az_span_copy(remainder, operation_id); + az_span_copy_u8(remainder, '\0'); + + if (out_mqtt_topic_length) + { + *out_mqtt_topic_length = (size_t)required_length; + } + + return AZ_OK; +} + +AZ_INLINE az_iot_provisioning_client_registration_state +_az_iot_provisioning_registration_state_default() +{ + return (az_iot_provisioning_client_registration_state){ .assigned_hub_hostname = AZ_SPAN_EMPTY, + .device_id = AZ_SPAN_EMPTY, + .error_code = AZ_IOT_STATUS_UNKNOWN, + .extended_error_code = 0, + .error_message = AZ_SPAN_EMPTY, + .error_tracking_id = AZ_SPAN_EMPTY, + .error_timestamp = AZ_SPAN_EMPTY }; +} + +AZ_INLINE az_iot_status _az_iot_status_from_extended_status(uint32_t extended_status) +{ + // NOLINTNEXTLINE(readability-magic-numbers, cppcoreguidelines-avoid-magic-numbers) + return (az_iot_status)(extended_status / 1000); +} + +/* +Documented at +https://docs.microsoft.com/rest/api/iot-dps/device/runtime-registration/register-device#deviceregistrationresult + "registrationState":{ + "x509":{}, + "registrationId":"paho-sample-device1", + "createdDateTimeUtc":"2020-04-10T03:11:13.0276997Z", + "assignedHub":"contoso.azure-devices.net", + "deviceId":"paho-sample-device1", + "status":"assigned", + "substatus":"initialAssignment", + "lastUpdatedDateTimeUtc":"2020-04-10T03:11:13.2096201Z", + "etag":"IjYxMDA4ZDQ2LTAwMDAtMDEwMC0wMDAwLTVlOGZlM2QxMDAwMCI="}} +*/ +AZ_INLINE az_result _az_iot_provisioning_client_parse_payload_error_code( + az_json_reader* jr, + az_iot_provisioning_client_registration_state* out_state) +{ + if (az_json_token_is_text_equal(&jr->token, AZ_SPAN_FROM_STR("errorCode"))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(jr)); + _az_RETURN_IF_FAILED(az_json_token_get_uint32(&jr->token, &out_state->extended_error_code)); + out_state->error_code = _az_iot_status_from_extended_status(out_state->extended_error_code); + + return AZ_OK; + } + + return AZ_ERROR_ITEM_NOT_FOUND; +} + +AZ_INLINE az_result _az_iot_provisioning_client_payload_registration_state_parse( + az_json_reader* jr, + az_iot_provisioning_client_registration_state* out_state) +{ + if (jr->token.kind != AZ_JSON_TOKEN_BEGIN_OBJECT) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + bool found_assigned_hub = false; + bool found_device_id = false; + + while ((!(found_device_id && found_assigned_hub)) + && az_result_succeeded(az_json_reader_next_token(jr)) + && jr->token.kind != AZ_JSON_TOKEN_END_OBJECT) + { + if (az_json_token_is_text_equal(&jr->token, AZ_SPAN_FROM_STR("assignedHub"))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(jr)); + if (jr->token.kind != AZ_JSON_TOKEN_STRING) + { + return AZ_ERROR_ITEM_NOT_FOUND; + } + out_state->assigned_hub_hostname = jr->token.slice; + found_assigned_hub = true; + } + else if (az_json_token_is_text_equal(&jr->token, AZ_SPAN_FROM_STR("deviceId"))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(jr)); + if (jr->token.kind != AZ_JSON_TOKEN_STRING) + { + return AZ_ERROR_ITEM_NOT_FOUND; + } + out_state->device_id = jr->token.slice; + found_device_id = true; + } + else if (az_json_token_is_text_equal(&jr->token, AZ_SPAN_FROM_STR("errorMessage"))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(jr)); + if (jr->token.kind != AZ_JSON_TOKEN_STRING) + { + return AZ_ERROR_ITEM_NOT_FOUND; + } + out_state->error_message = jr->token.slice; + } + else if (az_json_token_is_text_equal(&jr->token, AZ_SPAN_FROM_STR("lastUpdatedDateTimeUtc"))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(jr)); + if (jr->token.kind != AZ_JSON_TOKEN_STRING) + { + return AZ_ERROR_ITEM_NOT_FOUND; + } + out_state->error_timestamp = jr->token.slice; + } + else if (az_result_succeeded( + _az_iot_provisioning_client_parse_payload_error_code(jr, out_state))) + { + // Do nothing + } + else + { + // ignore other tokens + _az_RETURN_IF_FAILED(az_json_reader_skip_children(jr)); + } + } + + if (found_assigned_hub != found_device_id) + { + return AZ_ERROR_ITEM_NOT_FOUND; + } + + return AZ_OK; +} + +AZ_NODISCARD static az_result _az_iot_provisioning_client_parse_operation_status( + az_span response_operation_status, + az_iot_provisioning_client_operation_status* out_operation_status) +{ + _az_PRECONDITION_VALID_SPAN(response_operation_status, 0, false); + _az_PRECONDITION_NOT_NULL(out_operation_status); + + if (az_span_is_content_equal(response_operation_status, AZ_SPAN_FROM_STR("assigning"))) + { + *out_operation_status = AZ_IOT_PROVISIONING_STATUS_ASSIGNING; + } + else if (az_span_is_content_equal(response_operation_status, AZ_SPAN_FROM_STR("assigned"))) + { + *out_operation_status = AZ_IOT_PROVISIONING_STATUS_ASSIGNED; + } + else if (az_span_is_content_equal(response_operation_status, AZ_SPAN_FROM_STR("failed"))) + { + *out_operation_status = AZ_IOT_PROVISIONING_STATUS_FAILED; + } + else if (az_span_is_content_equal(response_operation_status, AZ_SPAN_FROM_STR("unassigned"))) + { + *out_operation_status = AZ_IOT_PROVISIONING_STATUS_UNASSIGNED; + } + else if (az_span_is_content_equal(response_operation_status, AZ_SPAN_FROM_STR("disabled"))) + { + *out_operation_status = AZ_IOT_PROVISIONING_STATUS_DISABLED; + } + else + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + return AZ_OK; +} + +AZ_INLINE az_result az_iot_provisioning_client_parse_payload( + az_span received_payload, + az_iot_provisioning_client_register_response* out_response) +{ + // Parse the payload: + az_json_reader jr; + _az_RETURN_IF_FAILED(az_json_reader_init(&jr, received_payload, NULL)); + + _az_RETURN_IF_FAILED(az_json_reader_next_token(&jr)); + if (jr.token.kind != AZ_JSON_TOKEN_BEGIN_OBJECT) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + out_response->registration_state = _az_iot_provisioning_registration_state_default(); + + bool found_operation_id = false; + bool found_operation_status = false; + bool found_error = false; + + while (az_result_succeeded(az_json_reader_next_token(&jr)) + && jr.token.kind != AZ_JSON_TOKEN_END_OBJECT) + { + if (az_json_token_is_text_equal(&jr.token, AZ_SPAN_FROM_STR("operationId"))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(&jr)); + if (jr.token.kind != AZ_JSON_TOKEN_STRING) + { + return AZ_ERROR_ITEM_NOT_FOUND; + } + out_response->operation_id = jr.token.slice; + found_operation_id = true; + } + else if (az_json_token_is_text_equal(&jr.token, AZ_SPAN_FROM_STR("status"))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(&jr)); + if (jr.token.kind != AZ_JSON_TOKEN_STRING) + { + return AZ_ERROR_ITEM_NOT_FOUND; + } + _az_RETURN_IF_FAILED(_az_iot_provisioning_client_parse_operation_status( + jr.token.slice, &out_response->operation_status)); + + found_operation_status = true; + } + else if (az_json_token_is_text_equal(&jr.token, AZ_SPAN_FROM_STR("registrationState"))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(&jr)); + _az_RETURN_IF_FAILED(_az_iot_provisioning_client_payload_registration_state_parse( + &jr, &out_response->registration_state)); + } + else if (az_json_token_is_text_equal(&jr.token, AZ_SPAN_FROM_STR("trackingId"))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(&jr)); + if (jr.token.kind != AZ_JSON_TOKEN_STRING) + { + return AZ_ERROR_ITEM_NOT_FOUND; + } + out_response->registration_state.error_tracking_id = jr.token.slice; + } + else if (az_json_token_is_text_equal(&jr.token, AZ_SPAN_FROM_STR("message"))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(&jr)); + if (jr.token.kind != AZ_JSON_TOKEN_STRING) + { + return AZ_ERROR_ITEM_NOT_FOUND; + } + out_response->registration_state.error_message = jr.token.slice; + } + else if (az_json_token_is_text_equal(&jr.token, AZ_SPAN_FROM_STR("timestampUtc"))) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(&jr)); + if (jr.token.kind != AZ_JSON_TOKEN_STRING) + { + return AZ_ERROR_ITEM_NOT_FOUND; + } + out_response->registration_state.error_timestamp = jr.token.slice; + } + else if (az_result_succeeded(_az_iot_provisioning_client_parse_payload_error_code( + &jr, &out_response->registration_state))) + { + found_error = true; + } + else + { + // ignore other tokens + _az_RETURN_IF_FAILED(az_json_reader_skip_children(&jr)); + } + } + + if (!(found_operation_status && found_operation_id)) + { + out_response->operation_id = AZ_SPAN_EMPTY; + out_response->operation_status = AZ_IOT_PROVISIONING_STATUS_FAILED; + + if (!found_error) + { + return AZ_ERROR_ITEM_NOT_FOUND; + } + } + + return AZ_OK; +} + +/* +Example flow: + +Stage 1: + topic: $dps/registrations/res/202/?$rid=1&retry-after=3 + payload: + {"operationId":"4.d0a671905ea5b2c8.e7173b7b-0e54-4aa0-9d20-aeb1b89e6c7d","status":"assigning"} + +Stage 2: + {"operationId":"4.d0a671905ea5b2c8.e7173b7b-0e54-4aa0-9d20-aeb1b89e6c7d","status":"assigning", + "registrationState":{"registrationId":"paho-sample-device1","status":"assigning"}} + +Stage 3: + topic: $dps/registrations/res/200/?$rid=1 + payload: + {"operationId":"4.d0a671905ea5b2c8.e7173b7b-0e54-4aa0-9d20-aeb1b89e6c7d","status":"assigned", + "registrationState":{ ... }} + + Error: + topic: $dps/registrations/res/401/?$rid=1 + payload: + {"errorCode":401002,"trackingId":"8ad0463c-6427-4479-9dfa-3e8bb7003e9b","message":"Invalid + certificate.","timestampUtc":"2020-04-10T05:24:22.4718526Z"} +*/ +AZ_NODISCARD az_result az_iot_provisioning_client_parse_received_topic_and_payload( + az_iot_provisioning_client const* client, + az_span received_topic, + az_span received_payload, + az_iot_provisioning_client_register_response* out_response) +{ + (void)client; + + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_VALID_SPAN(client->_internal.global_device_endpoint, 1, false); + _az_PRECONDITION_VALID_SPAN(received_topic, 1, false); + _az_PRECONDITION_VALID_SPAN(received_payload, 1, false); + _az_PRECONDITION_NOT_NULL(out_response); + + az_span str_dps_registrations_res = _az_iot_provisioning_get_dps_registrations_res(); + int32_t idx = az_span_find(received_topic, str_dps_registrations_res); + if (idx != 0) + { + return AZ_ERROR_IOT_TOPIC_NO_MATCH; + } + + _az_LOG_WRITE(AZ_LOG_MQTT_RECEIVED_TOPIC, received_topic); + _az_LOG_WRITE(AZ_LOG_MQTT_RECEIVED_PAYLOAD, received_payload); + + // Parse the status. + az_span remainder = az_span_slice_to_end(received_topic, az_span_size(str_dps_registrations_res)); + + int32_t index = 0; + az_span int_slice = _az_span_token(remainder, AZ_SPAN_FROM_STR("/"), &remainder, &index); + _az_RETURN_IF_FAILED(az_span_atou32(int_slice, (uint32_t*)(&out_response->status))); + + // Parse the optional retry-after= field. + az_span retry_after = AZ_SPAN_FROM_STR("retry-after="); + idx = az_span_find(remainder, retry_after); + if (idx != -1) + { + remainder = az_span_slice_to_end(remainder, idx + az_span_size(retry_after)); + int_slice = _az_span_token(remainder, AZ_SPAN_FROM_STR("&"), &remainder, &index); + + _az_RETURN_IF_FAILED(az_span_atou32(int_slice, &out_response->retry_after_seconds)); + } + else + { + out_response->retry_after_seconds = 0; + } + + _az_RETURN_IF_FAILED(az_iot_provisioning_client_parse_payload(received_payload, out_response)); + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_provisioning_client_get_request_payload( + az_iot_provisioning_client const* client, + az_span custom_payload_property, + az_iot_provisioning_client_payload_options const* options, + uint8_t* mqtt_payload, + size_t mqtt_payload_size, + size_t* out_mqtt_payload_length) +{ + (void)options; + + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_IS_NULL(options); + _az_PRECONDITION_NOT_NULL(mqtt_payload); + _az_PRECONDITION(mqtt_payload_size > 0); + _az_PRECONDITION_NOT_NULL(out_mqtt_payload_length); + + az_json_writer json_writer; + az_span payload_buffer = az_span_create(mqtt_payload, (int32_t)mqtt_payload_size); + + _az_RETURN_IF_FAILED(az_json_writer_init(&json_writer, payload_buffer, NULL)); + _az_RETURN_IF_FAILED(az_json_writer_append_begin_object(&json_writer)); + _az_RETURN_IF_FAILED( + az_json_writer_append_property_name(&json_writer, prov_registration_id_label)); + _az_RETURN_IF_FAILED( + az_json_writer_append_string(&json_writer, client->_internal.registration_id)); + + if (az_span_size(custom_payload_property) > 0) + { + _az_RETURN_IF_FAILED(az_json_writer_append_property_name(&json_writer, prov_payload_label)); + _az_RETURN_IF_FAILED(az_json_writer_append_json_text(&json_writer, custom_payload_property)); + } + + _az_RETURN_IF_FAILED(az_json_writer_append_end_object(&json_writer)); + *out_mqtt_payload_length + = (size_t)az_span_size(az_json_writer_get_bytes_used_in_destination(&json_writer)); + ; + + return AZ_OK; +} diff --git a/src/az_iot_provisioning_client.h b/src/az_iot_provisioning_client.h new file mode 100644 index 00000000..557e52a3 --- /dev/null +++ b/src/az_iot_provisioning_client.h @@ -0,0 +1,478 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file az_iot_provisioning_client.h + * + * @brief Definition for the Azure Device Provisioning client. + * @remark The Device Provisioning MQTT protocol is described at + * https://docs.microsoft.com/azure/iot-dps/iot-dps-mqtt-support + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_IOT_PROVISIONING_CLIENT_H +#define _az_IOT_PROVISIONING_CLIENT_H + +#include +#include +#include + +#include +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief The client is fixed to a specific version of the Azure IoT Provisioning service. + */ +#define AZ_IOT_PROVISIONING_SERVICE_VERSION "2019-03-31" + +/** + * @brief Azure IoT Provisioning Client options. + * + */ +typedef struct +{ + /** + * The user-agent is a formatted string that will be used for Azure IoT usage statistics. + */ + az_span user_agent; +} az_iot_provisioning_client_options; + +/** + * @brief Azure IoT Provisioning Client. + * + */ +typedef struct +{ + struct + { + az_span global_device_endpoint; + az_span id_scope; + az_span registration_id; + az_iot_provisioning_client_options options; + } _internal; +} az_iot_provisioning_client; + +/** + * @brief Gets the default Azure IoT Provisioning Client options. + * + * Call this to obtain an initialized #az_iot_provisioning_client_options structure that + * can be afterwards modified and passed to az_iot_provisioning_client_init(). + * + * @return #az_iot_provisioning_client_options. + */ +AZ_NODISCARD az_iot_provisioning_client_options az_iot_provisioning_client_options_default(); + +/** + * @brief Initializes an Azure IoT Provisioning Client. + * + * @param[in] client The #az_iot_provisioning_client to use for this call. + * @param[in] global_device_hostname The device provisioning services global host name. + * @param[in] id_scope The ID Scope. + * @param[in] registration_id The Registration ID. This must match the client certificate name (CN + * part of the certificate subject). + * @param[in] options __[nullable]__ A reference to an #az_iot_provisioning_client_options + * structure. Can be `NULL` for default options. + * @pre \p client must not be `NULL`. + * @pre \p global_device_hostname must be a valid span of size greater than 0. + * @pre \p id_scope must be a valid span of size greater than 0. + * @pre \p registration_id must be a valid span of size greater than 0. + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result az_iot_provisioning_client_init( + az_iot_provisioning_client* client, + az_span global_device_hostname, + az_span id_scope, + az_span registration_id, + az_iot_provisioning_client_options const* options); + +/** + * @brief Gets the MQTT user name. + * + * @param[in] client The #az_iot_provisioning_client to use for this call. + * @param[out] mqtt_user_name A buffer with sufficient capacity to hold the MQTT user name. If + * successful, contains a null-terminated string with the user name that needs to be passed to the + * MQTT client. + * @param[in] mqtt_user_name_size The size, in bytes of \p mqtt_user_name. + * @param[out] out_mqtt_user_name_length __[nullable]__ Contains the string length, in bytes, of \p + * mqtt_user_name. Can be `NULL`. + * @pre \p client must not be `NULL`. + * @pre \p mqtt_user_name must not be `NULL`. + * @pre \p mqtt_user_name_size must be greater than 0. + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result az_iot_provisioning_client_get_user_name( + az_iot_provisioning_client const* client, + char* mqtt_user_name, + size_t mqtt_user_name_size, + size_t* out_mqtt_user_name_length); + +/** + * @brief Gets the MQTT client id. + * + * @param[in] client The #az_iot_provisioning_client to use for this call. + * @param[out] mqtt_client_id A buffer with sufficient capacity to hold the MQTT client id. If + * successful, contains a null-terminated string with the client id that needs to be passed to the + * MQTT client. + * @param[in] mqtt_client_id_size The size, in bytes of \p mqtt_client_id. + * @param[out] out_mqtt_client_id_length __[nullable]__ Contains the string length, in bytes, of of + * \p mqtt_client_id. Can be `NULL`. + * @pre \p client must not be `NULL`. + * @pre \p mqtt_client_id must not be `NULL`. + * @pre \p mqtt_client_id_size must be greater than 0. + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result az_iot_provisioning_client_get_client_id( + az_iot_provisioning_client const* client, + char* mqtt_client_id, + size_t mqtt_client_id_size, + size_t* out_mqtt_client_id_length); + +/* + * + * SAS Token APIs + * + * Use the following APIs when the Shared Access Key is available to the application or stored + * within a Hardware Security Module. The APIs are not necessary if X509 Client Certificate + * Authentication is used. + * + * The TPM Asymmetric Device Provisioning protocol is not supported on the MQTT protocol. TPMs can + * still be used to securely store and perform HMAC-SHA256 operations for SAS tokens. + */ + +/** + * @brief Gets the Shared Access clear-text signature. + * + * The application must obtain a valid clear-text signature using this API, sign it using + * HMAC-SHA256 using the Shared Access Key as password then Base64 encode the result. + * + * @remark More information available at + * https://docs.microsoft.com/azure/iot-dps/concepts-symmetric-key-attestation#detailed-attestation-process + * + * @param[in] client The #az_iot_provisioning_client to use for this call. + * @param[in] token_expiration_epoch_time The time, in seconds, from 1/1/1970. + * @param[in] signature An empty #az_span with sufficient capacity to hold the SAS signature. + * @param[out] out_signature The output #az_span containing the SAS signature. + * @pre \p client must not be `NULL`. + * @pre \p token_expiration_epoch_time must be greater than 0. + * @pre \p signature must be a valid span of size greater than 0. + * @pre \p out_signature must not be `NULL`. + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result az_iot_provisioning_client_sas_get_signature( + az_iot_provisioning_client const* client, + uint64_t token_expiration_epoch_time, + az_span signature, + az_span* out_signature); + +/** + * @brief Gets the MQTT password. + * @remark The MQTT password must be an empty string if X509 Client certificates are used. Use this + * API only when authenticating with SAS tokens. + * + * @param[in] client The #az_iot_provisioning_client to use for this call. + * @param[in] base64_hmac_sha256_signature The Base64 encoded value of the HMAC-SHA256(signature, + * SharedAccessKey). The signature is obtained by using + * #az_iot_provisioning_client_sas_get_signature. + * @param[in] token_expiration_epoch_time The time, in seconds, from 1/1/1970. + * @param[in] key_name The Shared Access Key Name (Policy Name). This is optional. For security + * reasons we recommend using one key per device instead of using a global policy key. + * @param[out] mqtt_password A buffer with sufficient capacity to hold the MQTT password. If + * successful, contains a null-terminated string with the password that needs to be passed to the + * MQTT client. + * @param[in] mqtt_password_size The size, in bytes of \p mqtt_password. + * @param[out] out_mqtt_password_length __[nullable]__ Contains the string length, in bytes, of \p + * mqtt_password. Can be `NULL`. + * @pre \p client must not be `NULL`. + * @pre \p base64_hmac_sha256_signature must be a valid span of size greater than 0. + * @pre \p token_expiration_epoch_time must be greater than 0. + * @pre \p mqtt_password must not be `NULL`. + * @pre \p mqtt_password_size must be greater than 0. + * @return An #az_result value indicating the result of the operation.. + */ +AZ_NODISCARD az_result az_iot_provisioning_client_sas_get_password( + az_iot_provisioning_client const* client, + az_span base64_hmac_sha256_signature, + uint64_t token_expiration_epoch_time, + az_span key_name, + char* mqtt_password, + size_t mqtt_password_size, + size_t* out_mqtt_password_length); + +/* + * + * Register APIs + * + * Use the following APIs when the Shared Access Key is available to the application or stored + * within a Hardware Security Module. The APIs are not necessary if X509 Client Certificate + * Authentication is used. + */ + +/** + * @brief The MQTT topic filter to subscribe to register responses. + * @remark Register MQTT Publish messages will have QoS At most once (0). + */ +#define AZ_IOT_PROVISIONING_CLIENT_REGISTER_SUBSCRIBE_TOPIC "$dps/registrations/res/#" + +/** + * @brief The registration operation state. + * @remark This is returned only when the operation completed. + * + */ +typedef struct +{ + /** + * Assigned Azure IoT Hub hostname. + * @remark This is only available if `error_code` is success. + */ + az_span assigned_hub_hostname; + + /** + * Assigned device ID. + */ + az_span device_id; + + /** + * The error code. + */ + az_iot_status error_code; + + /** + * The extended, 6 digit error code. + */ + uint32_t extended_error_code; + + /** + * Error description. + */ + az_span error_message; + + /** + * Submit this ID when asking for Azure IoT service-desk help. + */ + az_span error_tracking_id; + + /** + * Submit this timestamp when asking for Azure IoT service-desk help. + */ + az_span error_timestamp; +} az_iot_provisioning_client_registration_state; + +/** + * @brief Azure IoT Provisioning Service operation status. + * + */ +typedef enum +{ + /** + * Starting state (not assigned). + */ + AZ_IOT_PROVISIONING_STATUS_UNASSIGNED, + /** + * Assigning in progress. + */ + AZ_IOT_PROVISIONING_STATUS_ASSIGNING, + + // Device assignment operation complete. + /** + * Device was assigned successfully. + */ + AZ_IOT_PROVISIONING_STATUS_ASSIGNED, + + /** + * The provisioning for the device failed. + */ + AZ_IOT_PROVISIONING_STATUS_FAILED, + + /** + * The provisioning for this device was disabled. + */ + AZ_IOT_PROVISIONING_STATUS_DISABLED, +} az_iot_provisioning_client_operation_status; + +/** + * @brief Register or query operation response. + * + */ +typedef struct +{ + /** + * The id of the register operation. + */ + az_span operation_id; + + // Avoid using enum as the first field within structs, to allow for { 0 } initialization. + // This is a workaround for IAR compiler warning [Pe188]: enumerated type mixed with another type. + + /** + * The current request status. + * @remark The authoritative response for the device registration operation (which may require + * several requests) is available only through #operation_status. + */ + az_iot_status status; + + /** + * The status of the register operation. + */ + az_iot_provisioning_client_operation_status operation_status; + + /** + * Recommended timeout before sending the next MQTT publish. + */ + uint32_t retry_after_seconds; + + /** + * If the operation is complete (success or error), the registration state will contain the hub + * and device id in case of success. + */ + az_iot_provisioning_client_registration_state registration_state; +} az_iot_provisioning_client_register_response; + +/** + * @brief Attempts to parse a received message's topic. + * + * @param[in] client The #az_iot_provisioning_client to use for this call. + * @param[in] received_topic An #az_span containing the received MQTT topic. + * @param[in] received_payload An #az_span containing the received MQTT payload. + * @param[out] out_response If the message is register-operation related, this will contain the + * #az_iot_provisioning_client_register_response. + * @pre \p client must not be `NULL`. + * @pre \p received_topic must be a valid span of size greater than or equal to 0. + * @pre \p received_payload must be a valid span of size greater than or equal to 0. + * @pre \p out_response must not be `NULL`. + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_ERROR_IOT_TOPIC_NO_MATCH If the topic is not matching the expected format. + */ +AZ_NODISCARD az_result az_iot_provisioning_client_parse_received_topic_and_payload( + az_iot_provisioning_client const* client, + az_span received_topic, + az_span received_payload, + az_iot_provisioning_client_register_response* out_response); + +/** + * @brief Checks if the status indicates that the service has an authoritative result of the + * register operation. The operation may have completed in either success or error. Completed + * states are: + * + * - #AZ_IOT_PROVISIONING_STATUS_ASSIGNED + * - #AZ_IOT_PROVISIONING_STATUS_FAILED + * - #AZ_IOT_PROVISIONING_STATUS_DISABLED + * + * @param[in] operation_status The status used to check if the operation completed. + * @return `true` if the operation completed. `false` otherwise. + */ +AZ_INLINE bool az_iot_provisioning_client_operation_complete( + az_iot_provisioning_client_operation_status operation_status) +{ + return (operation_status > AZ_IOT_PROVISIONING_STATUS_ASSIGNING); +} + +/** + * @brief Gets the MQTT topic that must be used to submit a Register request. + * @remark The payload of the MQTT publish message may contain a JSON document formatted according + * to the [Provisioning Service's Device Registration document] + * (https://docs.microsoft.com/rest/api/iot-dps/device/runtime-registration/register-device#deviceregistration) + * specification. + * + * @param[in] client The #az_iot_provisioning_client to use for this call. + * @param[out] mqtt_topic A buffer with sufficient capacity to hold the MQTT topic filter. If + * successful, contains a null-terminated string with the topic filter that needs to be passed to + * the MQTT client. + * @param[in] mqtt_topic_size The size, in bytes of \p mqtt_topic. + * @param[out] out_mqtt_topic_length __[nullable]__ Contains the string length, in bytes, of \p + * mqtt_topic. Can be `NULL`. + * @pre \p client must not be `NULL`. + * @pre \p mqtt_topic must not be `NULL`. + * @pre \p mqtt_topic_size must be greater than 0. + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result az_iot_provisioning_client_register_get_publish_topic( + az_iot_provisioning_client const* client, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length); + +/** + * @brief Gets the MQTT topic that must be used to submit a Register Status request. + * @remark The payload of the MQTT publish message should be empty. + * + * @param[in] client The #az_iot_provisioning_client to use for this call. + * @param[in] operation_id The received operation_id from the + * #az_iot_provisioning_client_register_response response. + * @param[out] mqtt_topic A buffer with sufficient capacity to hold the MQTT topic filter. If + * successful, contains a null-terminated string with the topic filter that needs to be passed to + * the MQTT client. + * @param[in] mqtt_topic_size The size, in bytes of \p mqtt_topic. + * @param[out] out_mqtt_topic_length __[nullable]__ Contains the string length, in bytes, of \p + * mqtt_topic. Can be `NULL`. + * @pre \p client must not be `NULL`. + * @pre \p operation_id must be a valid span of size greater than 0. + * @pre \p mqtt_topic must not be `NULL`. + * @pre \p mqtt_topic_size must be greater than 0. + * @return An #az_result value indicating the result of the operation. + */ +AZ_NODISCARD az_result az_iot_provisioning_client_query_status_get_publish_topic( + az_iot_provisioning_client const* client, + az_span operation_id, + char* mqtt_topic, + size_t mqtt_topic_size, + size_t* out_mqtt_topic_length); + +/** + * @brief Azure IoT Provisioning Client options for + * az_iot_provisioning_client_get_request_payload(). Not currently used. Reserved for future use. + * + */ +typedef struct +{ + struct + { + /// Currently, this is unused, but needed as a placeholder since we can't have an empty struct. + bool unused; + } _internal; +} az_iot_provisioning_client_payload_options; + +/** + * @brief Builds the optional payload for a provisioning request. + * @remark Use this API to build an MQTT payload during registration. + * This call is optional for most scenarios. Some service + * applications may require `custom_payload_property` specified during + * registration to take additional decisions during provisioning time. + * For example, if you need to register an IoT Plug and Play device you must + * specify its model_id with this API via the `custom_payload_property` + * `{"modelId":"your_model_id"}`. + * + * @param[in] client The #az_iot_provisioning_client to use for this call. + * @param[in] custom_payload_property __[nullable]__ Custom JSON to be added to this payload. + * Can be `NULL`. + * @param[in] options __[nullable]__ Reserved field for future options to this function. Must be + * `NULL`. + * @param[out] mqtt_payload A buffer with sufficient capacity to hold the MQTT payload. + * @param[in] mqtt_payload_size The size, in bytes of \p mqtt_payload. + * @param[out] out_mqtt_payload_length Contains the length, in bytes, written to \p mqtt_payload on + * success. + * @pre \p client must not be `NULL`. + * @pre \p options must be `NULL`. + * @pre \p mqtt_payload must not be `NULL`. + * @pre \p mqtt_payload_size must be greater than 0. + * @pre \p out_mqtt_payload_length must not be `NULL`. + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The payload was created successfully. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The buffer is too small. + */ +AZ_NODISCARD az_result az_iot_provisioning_client_get_request_payload( + az_iot_provisioning_client const* client, + az_span custom_payload_property, + az_iot_provisioning_client_payload_options const* options, + uint8_t* mqtt_payload, + size_t mqtt_payload_size, + size_t* out_mqtt_payload_length); + +#include <_az_cfg_suffix.h> + +#endif // _az_IOT_PROVISIONING_CLIENT_H diff --git a/src/az_iot_provisioning_client_sas.c b/src/az_iot_provisioning_client_sas.c new file mode 100644 index 00000000..18b7c097 --- /dev/null +++ b/src/az_iot_provisioning_client_sas.c @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include <_az_cfg.h> + +#define LF '\n' +#define AMPERSAND '&' +#define EQUAL_SIGN '=' +#define STRING_NULL_TERMINATOR '\0' +#define SCOPE_REGISTRATIONS_STRING "%2fregistrations%2f" +#define SAS_TOKEN_SR "SharedAccessSignature sr" +#define SAS_TOKEN_SE "se" +#define SAS_TOKEN_SIG "sig" +#define SAS_TOKEN_SKN "skn" + +static const az_span resources_string = AZ_SPAN_LITERAL_FROM_STR(SCOPE_REGISTRATIONS_STRING); +static const az_span sr_string = AZ_SPAN_LITERAL_FROM_STR(SAS_TOKEN_SR); +static const az_span sig_string = AZ_SPAN_LITERAL_FROM_STR(SAS_TOKEN_SIG); +static const az_span skn_string = AZ_SPAN_LITERAL_FROM_STR(SAS_TOKEN_SKN); +static const az_span se_string = AZ_SPAN_LITERAL_FROM_STR(SAS_TOKEN_SE); + +AZ_NODISCARD az_result az_iot_provisioning_client_sas_get_signature( + az_iot_provisioning_client const* client, + uint64_t token_expiration_epoch_time, + az_span signature, + az_span* out_signature) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION(token_expiration_epoch_time > 0); + _az_PRECONDITION_VALID_SPAN(signature, 0, false); + _az_PRECONDITION_NOT_NULL(out_signature); + + // Produces the following signature: + // url-encoded()\n + // Where + // resource-string: /registrations/ + + az_span remainder = signature; + int32_t signature_size = az_span_size(signature); + + _az_RETURN_IF_FAILED(_az_span_copy_url_encode(remainder, client->_internal.id_scope, &remainder)); + + _az_RETURN_IF_NOT_ENOUGH_SIZE(remainder, az_span_size(resources_string)); + remainder = az_span_copy(remainder, resources_string); + + _az_RETURN_IF_FAILED( + _az_span_copy_url_encode(remainder, client->_internal.registration_id, &remainder)); + + _az_RETURN_IF_NOT_ENOUGH_SIZE(remainder, 1 /* LF */); + remainder = az_span_copy_u8(remainder, LF); + + _az_RETURN_IF_FAILED(az_span_u64toa(remainder, token_expiration_epoch_time, &remainder)); + + *out_signature = az_span_slice(signature, 0, signature_size - az_span_size(remainder)); + _az_LOG_WRITE(AZ_LOG_IOT_SAS_TOKEN, *out_signature); + + return AZ_OK; +} + +AZ_NODISCARD az_result az_iot_provisioning_client_sas_get_password( + az_iot_provisioning_client const* client, + az_span base64_hmac_sha256_signature, + uint64_t token_expiration_epoch_time, + az_span key_name, + char* mqtt_password, + size_t mqtt_password_size, + size_t* out_mqtt_password_length) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_VALID_SPAN(base64_hmac_sha256_signature, 1, false); + _az_PRECONDITION(token_expiration_epoch_time > 0); + _az_PRECONDITION_NOT_NULL(mqtt_password); + _az_PRECONDITION(mqtt_password_size > 0); + + // Concatenates: + // "SharedAccessSignature sr=&sig=&se=" + // plus, if key_name is not NULL, "&skn=" + // + // Where: + // resource-string: /registrations/ + + az_span mqtt_password_span = az_span_create((uint8_t*)mqtt_password, (int32_t)mqtt_password_size); + + // SharedAccessSignature + _az_RETURN_IF_NOT_ENOUGH_SIZE(mqtt_password_span, az_span_size(sr_string) + 1 /* EQUAL SIGN */); + mqtt_password_span = az_span_copy(mqtt_password_span, sr_string); + mqtt_password_span = az_span_copy_u8(mqtt_password_span, EQUAL_SIGN); + + // Resource string + _az_RETURN_IF_FAILED(_az_span_copy_url_encode( + mqtt_password_span, client->_internal.id_scope, &mqtt_password_span)); + + _az_RETURN_IF_NOT_ENOUGH_SIZE(mqtt_password_span, az_span_size(resources_string)); + mqtt_password_span = az_span_copy(mqtt_password_span, resources_string); + + _az_RETURN_IF_FAILED(_az_span_copy_url_encode( + mqtt_password_span, client->_internal.registration_id, &mqtt_password_span)); + + // Signature + _az_RETURN_IF_NOT_ENOUGH_SIZE( + mqtt_password_span, 1 /* AMPERSAND */ + az_span_size(sig_string) + 1 /* EQUAL_SIGN */); + mqtt_password_span = az_span_copy_u8(mqtt_password_span, AMPERSAND); + mqtt_password_span = az_span_copy(mqtt_password_span, sig_string); + mqtt_password_span = az_span_copy_u8(mqtt_password_span, EQUAL_SIGN); + + _az_RETURN_IF_FAILED(_az_span_copy_url_encode( + mqtt_password_span, base64_hmac_sha256_signature, &mqtt_password_span)); + + // Expiration + + _az_RETURN_IF_NOT_ENOUGH_SIZE( + mqtt_password_span, + 1 /* AMPERSAND */ + az_span_size(se_string) + + 1 /* EQUAL_SIGN */ + _az_iot_u64toa_size(token_expiration_epoch_time)); + mqtt_password_span = az_span_copy_u8(mqtt_password_span, AMPERSAND); + mqtt_password_span = az_span_copy(mqtt_password_span, se_string); + mqtt_password_span = az_span_copy_u8(mqtt_password_span, EQUAL_SIGN); + _az_RETURN_IF_FAILED( + az_span_u64toa(mqtt_password_span, token_expiration_epoch_time, &mqtt_password_span)); + + if (az_span_size(key_name) > 0) + { + // Key Name + _az_RETURN_IF_NOT_ENOUGH_SIZE( + mqtt_password_span, + 1 // AMPERSAND + + az_span_size(skn_string) + 1 // EQUAL_SIGN + + az_span_size(key_name)); + + mqtt_password_span = az_span_copy_u8(mqtt_password_span, AMPERSAND); + mqtt_password_span = az_span_copy(mqtt_password_span, skn_string); + mqtt_password_span = az_span_copy_u8(mqtt_password_span, EQUAL_SIGN); + mqtt_password_span = az_span_copy(mqtt_password_span, key_name); + } + + _az_RETURN_IF_NOT_ENOUGH_SIZE(mqtt_password_span, 1 /* NULL TERMINATOR */); + mqtt_password_span = az_span_copy_u8(mqtt_password_span, STRING_NULL_TERMINATOR); + + if (out_mqtt_password_length != NULL) + { + *out_mqtt_password_length + = (mqtt_password_size - (size_t)az_span_size(mqtt_password_span) - 1 /* NULL TERMINATOR */); + } + + return AZ_OK; +} diff --git a/src/az_json.h b/src/az_json.h new file mode 100644 index 00000000..7a8ec6ff --- /dev/null +++ b/src/az_json.h @@ -0,0 +1,773 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief This header defines the types and functions your application uses to read or write JSON + * objects. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_JSON_H +#define _az_JSON_H + +#include +#include + +#include +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief Defines symbols for the various kinds of JSON tokens that make up any JSON text. + */ +typedef enum +{ + AZ_JSON_TOKEN_NONE, ///< There is no value (as distinct from #AZ_JSON_TOKEN_NULL). + AZ_JSON_TOKEN_BEGIN_OBJECT, ///< The token kind is the start of a JSON object. + AZ_JSON_TOKEN_END_OBJECT, ///< The token kind is the end of a JSON object. + AZ_JSON_TOKEN_BEGIN_ARRAY, ///< The token kind is the start of a JSON array. + AZ_JSON_TOKEN_END_ARRAY, ///< The token kind is the end of a JSON array. + AZ_JSON_TOKEN_PROPERTY_NAME, ///< The token kind is a JSON property name. + AZ_JSON_TOKEN_STRING, ///< The token kind is a JSON string. + AZ_JSON_TOKEN_NUMBER, ///< The token kind is a JSON number. + AZ_JSON_TOKEN_TRUE, ///< The token kind is the JSON literal `true`. + AZ_JSON_TOKEN_FALSE, ///< The token kind is the JSON literal `false`. + AZ_JSON_TOKEN_NULL, ///< The token kind is the JSON literal `null`. +} az_json_token_kind; + +/** + * @brief A limited stack used by the #az_json_writer and #az_json_reader to track state information + * for processing and validation. + */ +typedef struct +{ + struct + { + // This uint64_t container represents a tiny stack to track the state during nested transitions. + // The first bit represents the state of the current depth (1 == object, 0 == array). + // Each subsequent bit is the parent / containing type (object or array). + uint64_t az_json_stack; + int32_t current_depth; + } _internal; +} _az_json_bit_stack; + +/** + * @brief Represents a JSON token. The kind field indicates the type of the JSON token and the slice + * represents the portion of the JSON payload that points to the token value. + * + * @remarks An instance of #az_json_token must not outlive the lifetime of the #az_json_reader it + * came from. + */ +typedef struct +{ + /// This read-only field gives access to the slice of the JSON text that represents the token + /// value, and it shouldn't be modified by the caller. + /// If the token straddles non-contiguous buffers, this is set to the partial token value + /// available in the last segment. + /// The user can call #az_json_token_copy_into_span() to get the token value into a contiguous + /// buffer. + /// In the case of JSON strings, the slice does not include the surrounding quotes. + az_span slice; + + // Avoid using enum as the first field within structs, to allow for { 0 } initialization. + // This is a workaround for IAR compiler warning [Pe188]: enumerated type mixed with another type. + + /// This read-only field gives access to the type of the token returned by the #az_json_reader, + /// and it shouldn't be modified by the caller. + az_json_token_kind kind; + + /// This read-only field gives access to the size of the JSON text slice that represents the token + /// value, and it shouldn't be modified by the caller. This is useful if the token straddles + /// non-contiguous buffers, to figure out what sized destination buffer to provide when calling + /// #az_json_token_copy_into_span(). + int32_t size; + + struct + { + /// A flag to indicate whether the JSON token straddles more than one buffer segment and is + /// split amongst non-contiguous buffers. For tokens created from input JSON payloads within a + /// contiguous buffer, this field is always false. + bool is_multisegment; + + /// A flag to indicate whether the JSON string contained any escaped characters, used as an + /// optimization to avoid redundant checks. It is meaningless for any other token kind. + bool string_has_escaped_chars; + + /// This is the first segment in the entire JSON payload, if it was non-contiguous. Otherwise, + /// its set to #AZ_SPAN_EMPTY. + az_span* pointer_to_first_buffer; + + /// The segment index within the non-contiguous JSON payload where this token starts. + int32_t start_buffer_index; + + /// The offset within the particular segment within which this token starts. + int32_t start_buffer_offset; + + /// The segment index within the non-contiguous JSON payload where this token ends. + int32_t end_buffer_index; + + /// The offset within the particular segment within which this token ends. + int32_t end_buffer_offset; + } _internal; +} az_json_token; + +// TODO: Should the parameters be reversed? +/** + * @brief Copies the content of the \p token #az_json_token to the \p destination #az_span. + * + * @param[in] json_token A pointer to an #az_json_token instance containing the JSON text to copy to + * the \p destination. + * @param destination The #az_span whose bytes will be replaced by the JSON text from the \p + * json_token. + * + * @return An #az_span that is a slice of the \p destination #az_span (i.e. the remainder) after the + * token bytes have been copied. + * + * @remarks The function assumes that the \p destination has a large enough size to hold the + * contents of \p json_token. + * + * @remarks If \p json_token doesn't contain any text, this function will just return \p + * destination. + */ +az_span az_json_token_copy_into_span(az_json_token const* json_token, az_span destination); + +/** + * @brief Gets the JSON token's boolean. + * + * @param[in] json_token A pointer to an #az_json_token instance. + * @param[out] out_value A pointer to a variable to receive the value. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The boolean value is returned. + * @retval #AZ_ERROR_JSON_INVALID_STATE The kind is not #AZ_JSON_TOKEN_TRUE or #AZ_JSON_TOKEN_FALSE. + */ +AZ_NODISCARD az_result az_json_token_get_boolean(az_json_token const* json_token, bool* out_value); + +/** + * @brief Gets the JSON token's number as a 64-bit unsigned integer. + * + * @param[in] json_token A pointer to an #az_json_token instance. + * @param[out] out_value A pointer to a variable to receive the value. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The number is returned. + * @retval #AZ_ERROR_JSON_INVALID_STATE The kind is not #AZ_JSON_TOKEN_NUMBER. + * @retval #AZ_ERROR_UNEXPECTED_CHAR A non-ASCII digit is found within the \p json_token or \p + * json_token contains a number that would overflow or underflow `uint64_t`. + */ +AZ_NODISCARD az_result +az_json_token_get_uint64(az_json_token const* json_token, uint64_t* out_value); + +/** + * @brief Gets the JSON token's number as a 32-bit unsigned integer. + * + * @param[in] json_token A pointer to an #az_json_token instance. + * @param[out] out_value A pointer to a variable to receive the value. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The number is returned. + * @retval #AZ_ERROR_JSON_INVALID_STATE The kind is not #AZ_JSON_TOKEN_NUMBER. + * @retval #AZ_ERROR_UNEXPECTED_CHAR A non-ASCII digit is found within the token or if it contains a + * number that would overflow or underflow `uint32_t`. + */ +AZ_NODISCARD az_result +az_json_token_get_uint32(az_json_token const* json_token, uint32_t* out_value); + +/** + * @brief Gets the JSON token's number as a 64-bit signed integer. + * + * @param[in] json_token A pointer to an #az_json_token instance. + * @param[out] out_value A pointer to a variable to receive the value. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The number is returned. + * @retval #AZ_ERROR_JSON_INVALID_STATE The kind is not #AZ_JSON_TOKEN_NUMBER. + * @retval #AZ_ERROR_UNEXPECTED_CHAR A non-ASCII digit is found within the token or if it contains + * a number that would overflow or underflow `int64_t`. + */ +AZ_NODISCARD az_result az_json_token_get_int64(az_json_token const* json_token, int64_t* out_value); + +/** + * @brief Gets the JSON token's number as a 32-bit signed integer. + * + * @param[in] json_token A pointer to an #az_json_token instance. + * @param[out] out_value A pointer to a variable to receive the value. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The number is returned. + * @retval #AZ_ERROR_JSON_INVALID_STATE The kind is not #AZ_JSON_TOKEN_NUMBER. + * @retval #AZ_ERROR_UNEXPECTED_CHAR A non-ASCII digit is found within the token or if it contains a + * number that would overflow or underflow `int32_t`. + */ +AZ_NODISCARD az_result az_json_token_get_int32(az_json_token const* json_token, int32_t* out_value); + +/** + * @brief Gets the JSON token's number as a `double`. + * + * @param[in] json_token A pointer to an #az_json_token instance. + * @param[out] out_value A pointer to a variable to receive the value. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The number is returned. + * @retval #AZ_ERROR_JSON_INVALID_STATE The kind is not #AZ_JSON_TOKEN_NUMBER. + * @retval #AZ_ERROR_UNEXPECTED_CHAR The resulting \p out_value wouldn't be a finite double number. + */ +AZ_NODISCARD az_result az_json_token_get_double(az_json_token const* json_token, double* out_value); + +/** + * @brief Gets the JSON token's string after unescaping it, if required. + * + * @param[in] json_token A pointer to an #az_json_token instance. + * @param destination A pointer to a buffer where the string should be copied into. + * @param[in] destination_max_size The maximum available space within the buffer referred to by + * \p destination. + * @param[out] out_string_length __[nullable]__ Contains the number of bytes written to the \p + * destination which denote the length of the unescaped string. If `NULL` is passed, the parameter + * is ignored. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The string is returned. + * @retval #AZ_ERROR_JSON_INVALID_STATE The kind is not #AZ_JSON_TOKEN_STRING. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE \p destination does not have enough size. + */ +AZ_NODISCARD az_result az_json_token_get_string( + az_json_token const* json_token, + char* destination, + int32_t destination_max_size, + int32_t* out_string_length); + +/** + * @brief Determines whether the unescaped JSON token value that the #az_json_token points to is + * equal to the expected text within the provided byte span by doing a case-sensitive comparison. + * + * @param[in] json_token A pointer to an #az_json_token instance containing the JSON string token. + * @param[in] expected_text The lookup text to compare the token against. + * + * @return `true` if the current JSON token value in the JSON source semantically matches the + * expected lookup text, with the exact casing; otherwise, `false`. + * + * @remarks This operation is only valid for the string and property name token kinds. For all other + * token kinds, it returns false. + */ +AZ_NODISCARD bool az_json_token_is_text_equal( + az_json_token const* json_token, + az_span expected_text); + +/************************************ JSON WRITER ******************/ + +/** + * @brief Allows the user to define custom behavior when writing JSON using the #az_json_writer. + */ +typedef struct +{ + struct + { + /// Currently, this is unused, but needed as a placeholder since we can't have an empty struct. + bool unused; + } _internal; +} az_json_writer_options; + +/** + * @brief Gets the default json writer options which builds minimized JSON (with no extra white + * space) according to the JSON RFC. + * + * @details Call this to obtain an initialized #az_json_writer_options structure that can be + * modified and passed to #az_json_writer_init(). + * + * @return The default #az_json_writer_options. + */ +AZ_NODISCARD AZ_INLINE az_json_writer_options az_json_writer_options_default() +{ + az_json_writer_options options = (az_json_writer_options) { + ._internal = { + .unused = false, + }, + }; + + return options; +} + +/** + * @brief Provides forward-only, non-cached writing of UTF-8 encoded JSON text into the provided + * buffer. + * + * @remarks #az_json_writer builds the text sequentially with no caching and by default adheres to + * the JSON RFC: https://tools.ietf.org/html/rfc8259. + */ +typedef struct +{ + struct + { + az_span destination_buffer; + int32_t bytes_written; + // For single contiguous buffer, bytes_written == total_bytes_written + int32_t total_bytes_written; // Currently, this is primarily used for testing. + az_span_allocator_fn allocator_callback; + void* user_context; + bool need_comma; + az_json_token_kind token_kind; // needed for validation, potentially #if/def with preconditions. + _az_json_bit_stack bit_stack; // needed for validation, potentially #if/def with preconditions. + az_json_writer_options options; + } _internal; +} az_json_writer; + +/** + * @brief Initializes an #az_json_writer which writes JSON text into a buffer. + * + * @param[out] out_json_writer A pointer to an #az_json_writer instance to initialize. + * @param destination_buffer An #az_span over the byte buffer where the JSON text is to be written. + * @param[in] options __[nullable]__ A reference to an #az_json_writer_options + * structure which defines custom behavior of the #az_json_writer. If `NULL` is passed, the writer + * will use the default options (i.e. #az_json_writer_options_default()). + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK #az_json_writer is initialized successfully. + * @retval other Initialization failed. + */ +AZ_NODISCARD az_result az_json_writer_init( + az_json_writer* out_json_writer, + az_span destination_buffer, + az_json_writer_options const* options); + +/** + * @brief Initializes an #az_json_writer which writes JSON text into a destination that can contain + * non-contiguous buffers. + * + * @param[out] out_json_writer A pointer to an #az_json_writer the instance to initialize. + * @param[in] first_destination_buffer An #az_span over the byte buffer where the JSON text is to be + * written at the start. + * @param[in] allocator_callback An #az_span_allocator_fn callback function that provides the + * destination span to write the JSON text to once the previous buffer is full or too small to + * contain the next token. + * @param user_context A context specific user-defined struct or set of fields that is passed + * through to calls to the #az_span_allocator_fn. + * @param[in] options __[nullable]__ A reference to an #az_json_writer_options + * structure which defines custom behavior of the #az_json_writer. If `NULL` is passed, the writer + * will use the default options (i.e. #az_json_writer_options_default()). + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The #az_json_writer is initialized successfully. + * @retval other Failure. + */ +AZ_NODISCARD az_result az_json_writer_chunked_init( + az_json_writer* out_json_writer, + az_span first_destination_buffer, + az_span_allocator_fn allocator_callback, + void* user_context, + az_json_writer_options const* options); + +/** + * @brief Returns the #az_span containing the JSON text written to the underlying buffer so far, in + * the last provided destination buffer. + * + * @param[in] json_writer A pointer to an #az_json_writer instance wrapping the destination buffer. + * + * @note Do NOT modify or override the contents of the returned #az_span unless you are no longer + * writing JSON text into it. + * + * @return An #az_span containing the JSON text built so far. + * + * @remarks This function returns the entire JSON text when it fits in the first provided buffer, + * where the destination is a single, contiguous buffer. When the destination can be a set of + * non-contiguous buffers (using #az_json_writer_chunked_init()), and the JSON is larger than the + * first provided destination span, this function only returns the text written into the last + * provided destination buffer from the allocator callback. + */ +AZ_NODISCARD AZ_INLINE az_span +az_json_writer_get_bytes_used_in_destination(az_json_writer const* json_writer) +{ + return az_span_slice( + json_writer->_internal.destination_buffer, 0, json_writer->_internal.bytes_written); +} + +/** + * @brief Appends the UTF-8 text value (as a JSON string) into the buffer. + * + * @note If you receive an #AZ_ERROR_NOT_ENOUGH_SPACE result while appending data for which there is + * theoretically space, note that the JSON writer requires at least 64-bytes of slack within the + * output buffer, above the theoretical minimal space needed. The JSON writer pessimistically + * requires at least 64-bytes of space when writing any chunk of data larger than 10 characters + * because it tries to write in 64 byte chunks (10 character * 6 if all need to be escaped into the + * unicode form). + * + * @param[in,out] ref_json_writer A pointer to an #az_json_writer instance containing the buffer to + * append the string value to. + * @param[in] value The UTF-8 encoded value to be written as a JSON string. The value is escaped + * before writing. + * + * @remarks If \p value is #AZ_SPAN_EMPTY, the empty JSON string value is written (i.e. ""). + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The string value was appended successfully. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The buffer is too small. + */ +AZ_NODISCARD az_result az_json_writer_append_string(az_json_writer* ref_json_writer, az_span value); + +/** + * @brief Appends an existing UTF-8 encoded JSON text into the buffer, useful for appending nested + * JSON. + * + * @param[in,out] ref_json_writer A pointer to an #az_json_writer instance containing the buffer to + * append the JSON text to. + * @param[in] json_text A single, possibly nested, valid, UTF-8 encoded, JSON value to be written as + * is, without any formatting or spacing changes. No modifications are made to this text, including + * escaping. + * + * @remarks A single, possibly nested, JSON value is one that starts and ends with {} or [] or is a + * single primitive token. The JSON cannot start with an end object or array, or a property name, or + * be incomplete. + * + * @remarks The function validates that the provided JSON to be appended is valid and properly + * escaped, and fails otherwise. + * + * @note If you receive an #AZ_ERROR_NOT_ENOUGH_SPACE result while appending data for which there is + * theoretically space, note that the JSON writer requires at least 64-bytes of slack within the + * output buffer, above the theoretical minimal space needed. The JSON writer pessimistically + * requires at least 64-bytes of space when writing any chunk of data larger than 10 characters + * because it tries to write in 64 byte chunks (10 character * 6 if all need to be escaped into the + * unicode form). + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The provided \p json_text was appended successfully. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The destination is too small for the provided \p json_text. + * @retval #AZ_ERROR_JSON_INVALID_STATE The \p ref_json_writer is in a state where the \p json_text + * cannot be appended because it would result in invalid JSON. + * @retval #AZ_ERROR_UNEXPECTED_END The provided \p json_text is invalid because it is incomplete + * and ends too early. + * @retval #AZ_ERROR_UNEXPECTED_CHAR The provided \p json_text is invalid because of an unexpected + * character. + */ +AZ_NODISCARD az_result +az_json_writer_append_json_text(az_json_writer* ref_json_writer, az_span json_text); + +/** + * @brief Appends the UTF-8 property name (as a JSON string) which is the first part of a name/value + * pair of a JSON object. + * + * @param[in,out] ref_json_writer A pointer to an #az_json_writer instance containing the buffer to + * append the property name to. + * @param[in] name The UTF-8 encoded property name of the JSON value to be written. The name is + * escaped before writing. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The property name was appended successfully. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The buffer is too small. + */ +AZ_NODISCARD az_result +az_json_writer_append_property_name(az_json_writer* ref_json_writer, az_span name); + +/** + * @brief Appends a boolean value (as a JSON literal `true` or `false`). + * + * @param[in,out] ref_json_writer A pointer to an #az_json_writer instance containing the buffer to + * append the boolean to. + * @param[in] value The value to be written as a JSON literal `true` or `false`. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The boolean was appended successfully. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The buffer is too small. + */ +AZ_NODISCARD az_result az_json_writer_append_bool(az_json_writer* ref_json_writer, bool value); + +/** + * @brief Appends an `int32_t` number value. + * + * @param[in,out] ref_json_writer A pointer to an #az_json_writer instance containing the buffer to + * append the number to. + * @param[in] value The value to be written as a JSON number. + * + * @note If you receive an #AZ_ERROR_NOT_ENOUGH_SPACE result while appending data for which there is + * theoretically space, note that the JSON writer requires at least 64-bytes of slack within the + * output buffer, above the theoretical minimal space needed. The JSON writer pessimistically + * requires at least 64-bytes of space when writing any chunk of data larger than 10 characters + * because it tries to write in 64 byte chunks (10 character * 6 if all need to be escaped into the + * unicode form). + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The number was appended successfully. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The buffer is too small. + */ +AZ_NODISCARD az_result az_json_writer_append_int32(az_json_writer* ref_json_writer, int32_t value); + +/** + * @brief Appends a `double` number value. + * + * @param[in,out] ref_json_writer A pointer to an #az_json_writer instance containing the buffer to + * append the number to. + * @param[in] value The value to be written as a JSON number. + * @param[in] fractional_digits The number of digits of the \p value to write after the decimal + * point and truncate the rest. + * + * @note If you receive an #AZ_ERROR_NOT_ENOUGH_SPACE result while appending data for which there is + * theoretically space, note that the JSON writer requires at least 64-bytes of slack within the + * output buffer, above the theoretical minimal space needed. The JSON writer pessimistically + * requires at least 64-bytes of space when writing any chunk of data larger than 10 characters + * because it tries to write in 64 byte chunks (10 character * 6 if all need to be escaped into the + * unicode form). + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The number was appended successfully. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The buffer is too small. + * @retval #AZ_ERROR_NOT_SUPPORTED The \p value contains an integer component that is too large and + * would overflow beyond `2^53 - 1`. + * + * @remark Only finite double values are supported. Values such as `NAN` and `INFINITY` are not + * allowed and would lead to invalid JSON being written. + * + * @remark Non-significant trailing zeros (after the decimal point) are not written, even if \p + * fractional_digits is large enough to allow the zero padding. + * + * @remark The \p fractional_digits must be between 0 and 15 (inclusive). Any value passed in that + * is larger will be clamped down to 15. + */ +AZ_NODISCARD az_result az_json_writer_append_double( + az_json_writer* ref_json_writer, + double value, + int32_t fractional_digits); + +/** + * @brief Appends the JSON literal `null`. + * + * @param[in,out] ref_json_writer A pointer to an #az_json_writer instance containing the buffer to + * append the `null` literal to. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK `null` was appended successfully. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The buffer is too small. + */ +AZ_NODISCARD az_result az_json_writer_append_null(az_json_writer* ref_json_writer); + +/** + * @brief Appends the beginning of a JSON object (i.e. `{`). + * + * @param[in,out] ref_json_writer A pointer to an #az_json_writer instance containing the buffer to + * append the start of object to. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Object start was appended successfully. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The buffer is too small. + * @retval #AZ_ERROR_JSON_NESTING_OVERFLOW The depth of the JSON exceeds the maximum allowed + * depth of 64. + */ +AZ_NODISCARD az_result az_json_writer_append_begin_object(az_json_writer* ref_json_writer); + +/** + * @brief Appends the beginning of a JSON array (i.e. `[`). + * + * @param[in,out] ref_json_writer A pointer to an #az_json_writer instance containing the buffer to + * append the start of array to. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Array start was appended successfully. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The buffer is too small. + * @retval #AZ_ERROR_JSON_NESTING_OVERFLOW The depth of the JSON exceeds the maximum allowed depth + * of 64. + */ +AZ_NODISCARD az_result az_json_writer_append_begin_array(az_json_writer* ref_json_writer); + +/** + * @brief Appends the end of the current JSON object (i.e. `}`). + * + * @param[in,out] ref_json_writer A pointer to an #az_json_writer instance containing the buffer to + * append the closing character to. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Object end was appended successfully. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The buffer is too small. + */ +AZ_NODISCARD az_result az_json_writer_append_end_object(az_json_writer* ref_json_writer); + +/** + * @brief Appends the end of the current JSON array (i.e. `]`). + * + * @param[in,out] ref_json_writer A pointer to an #az_json_writer instance containing the buffer to + * append the closing character to. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Array end was appended successfully. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The buffer is too small. + */ +AZ_NODISCARD az_result az_json_writer_append_end_array(az_json_writer* ref_json_writer); + +/************************************ JSON READER ******************/ + +/** + * @brief Allows the user to define custom behavior when reading JSON using the #az_json_reader. + */ +typedef struct +{ + struct + { + /// Currently, this is unused, but needed as a placeholder since we can't have an empty struct. + bool unused; + } _internal; +} az_json_reader_options; + +/** + * @brief Gets the default json reader options which reads the JSON strictly according to the JSON + * RFC. + * + * @details Call this to obtain an initialized #az_json_reader_options structure that can be + * modified and passed to #az_json_reader_init(). + * + * @return The default #az_json_reader_options. + */ +AZ_NODISCARD AZ_INLINE az_json_reader_options az_json_reader_options_default() +{ + az_json_reader_options options = (az_json_reader_options) { + ._internal = { + .unused = false, + }, + }; + + return options; +} + +/** + * @brief Returns the JSON tokens contained within a JSON buffer, one at a time. + * + * @remarks The token field is meant to be used as read-only to return the #az_json_token while + * reading the JSON. Do NOT modify it. + */ +typedef struct +{ + /// This read-only field gives access to the current token that the #az_json_reader has processed, + /// and it shouldn't be modified by the caller. + az_json_token token; + + /// The depth of the current token. This read-only field tracks the recursive depth of the nested + /// objects or arrays within the JSON text processed so far, and it shouldn't be modified by the + /// caller. + int32_t current_depth; + + struct + { + /// The first buffer containing the JSON payload. + az_span json_buffer; + + /// The array of non-contiguous buffers containing the JSON payload, which will be null for the + /// single buffer case. + az_span* json_buffers; + + /// The number of non-contiguous buffer segments in the array. It is set to one for the single + /// buffer case. + int32_t number_of_buffers; + + /// The current buffer segment being processed while reading the JSON in non-contiguous buffer + /// segments. + int32_t buffer_index; + + /// The number of bytes consumed so far in the current buffer segment. + int32_t bytes_consumed; + + /// The total bytes consumed from the input JSON payload. In the case of a single buffer, this + /// is identical to bytes_consumed. + int32_t total_bytes_consumed; + + /// Flag which indicates that we have a JSON object or array in the payload, rather than a + /// single primitive token (string, number, true, false, null). + bool is_complex_json; + + /// A limited stack to track the depth and nested JSON objects or arrays read so far. + _az_json_bit_stack bit_stack; + + /// A copy of the options provided by the user. + az_json_reader_options options; + } _internal; +} az_json_reader; + +/** + * @brief Initializes an #az_json_reader to read the JSON payload contained within the provided + * buffer. + * + * @param[out] out_json_reader A pointer to an #az_json_reader instance to initialize. + * @param[in] json_buffer An #az_span over the byte buffer containing the JSON text to read. + * @param[in] options __[nullable]__ A reference to an #az_json_reader_options structure which + * defines custom behavior of the #az_json_reader. If `NULL` is passed, the reader will use the + * default options (i.e. #az_json_reader_options_default()). + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The #az_json_reader is initialized successfully. + * @retval other Initialization failed. + * + * @remarks The provided json buffer must not be empty, as that is invalid JSON. + * + * @remarks An instance of #az_json_reader must not outlive the lifetime of the JSON payload within + * the \p json_buffer. + */ +AZ_NODISCARD az_result az_json_reader_init( + az_json_reader* out_json_reader, + az_span json_buffer, + az_json_reader_options const* options); + +/** + * @brief Initializes an #az_json_reader to read the JSON payload contained within the provided + * set of discontiguous buffers. + * + * @param[out] out_json_reader A pointer to an #az_json_reader instance to initialize. + * @param[in] json_buffers An array of non-contiguous byte buffers, as spans, containing the JSON + * text to read. + * @param[in] number_of_buffers The number of buffer segments provided, i.e. the length of the \p + * json_buffers array. + * @param[in] options __[nullable]__ A reference to an #az_json_reader_options + * structure which defines custom behavior of the #az_json_reader. If `NULL` is passed, the reader + * will use the default options (i.e. #az_json_reader_options_default()). + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The #az_json_reader is initialized successfully. + * @retval other Initialization failed. + * + * @remarks The provided array of json buffers must not be empty, as that is invalid JSON, and + * therefore \p number_of_buffers must also be greater than 0. The array must also not contain any + * empty span segments. + * + * @remarks An instance of #az_json_reader must not outlive the lifetime of the JSON payload within + * the \p json_buffers. + */ +AZ_NODISCARD az_result az_json_reader_chunked_init( + az_json_reader* out_json_reader, + az_span json_buffers[], + int32_t number_of_buffers, + az_json_reader_options const* options); + +/** + * @brief Reads the next token in the JSON text and updates the reader state. + * + * @param[in,out] ref_json_reader A pointer to an #az_json_reader instance containing the JSON to + * read. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The token was read successfully. + * @retval #AZ_ERROR_UNEXPECTED_END The end of the JSON document is reached. + * @retval #AZ_ERROR_UNEXPECTED_CHAR An invalid character is detected. + * @retval #AZ_ERROR_JSON_READER_DONE No more JSON text left to process. + */ +AZ_NODISCARD az_result az_json_reader_next_token(az_json_reader* ref_json_reader); + +/** + * @brief Reads and skips over any nested JSON elements. + * + * @param[in,out] ref_json_reader A pointer to an #az_json_reader instance containing the JSON to + * read. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK The children of the current JSON token are skipped successfully. + * @retval #AZ_ERROR_UNEXPECTED_END The end of the JSON document is reached. + * @retval #AZ_ERROR_UNEXPECTED_CHAR An invalid character is detected. + * + * @remarks If the current token kind is a property name, the reader first moves to the property + * value. Then, if the token kind is start of an object or array, the reader moves to the matching + * end object or array. For all other token kinds, the reader doesn't move and returns #AZ_OK. + */ +AZ_NODISCARD az_result az_json_reader_skip_children(az_json_reader* ref_json_reader); + +#include <_az_cfg_suffix.h> + +#endif // _az_JSON_H diff --git a/src/az_json_private.h b/src/az_json_private.h new file mode 100644 index 00000000..7fc23eec --- /dev/null +++ b/src/az_json_private.h @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#ifndef _az_JSON_PRIVATE_H +#define _az_JSON_PRIVATE_H + +#include +#include + +#include <_az_cfg_prefix.h> + +#define _az_JSON_TOKEN_DEFAULT \ + (az_json_token) \ + { \ + .kind = AZ_JSON_TOKEN_NONE, ._internal = { 0 } \ + } + +enum +{ + // We are using a uint64_t to represent our nested state, so we can only go 64 levels deep. + // This is safe to do because sizeof will not dereference the pointer and is used to find the size + // of the field used as the stack. + _az_MAX_JSON_STACK_SIZE = sizeof(((_az_json_bit_stack*)0)->_internal.az_json_stack) * 8 // 64 +}; + +enum +{ + // Max size for an already escaped string value (~ half of INT_MAX) + _az_MAX_ESCAPED_STRING_SIZE = 1000000000, + + // In the worst case, an ASCII character represented as a single UTF-8 byte could expand 6x when + // escaped. + // For example: '+' becomes '\u0043' + // Escaping surrogate pairs (represented by 3 or 4 UTF-8 bytes) would expand to 12 bytes (which is + // still <= 6x). + _az_MAX_EXPANSION_FACTOR_WHILE_ESCAPING = 6, + + _az_MAX_UNESCAPED_STRING_SIZE + = _az_MAX_ESCAPED_STRING_SIZE / _az_MAX_EXPANSION_FACTOR_WHILE_ESCAPING, // 166_666_666 bytes + + // [-][0-9]{16}.[0-9]{15}, i.e. 1+16+1+15 since _az_MAX_SUPPORTED_FRACTIONAL_DIGITS is 15 + _az_MAX_SIZE_FOR_WRITING_DOUBLE = 33, + + // When writing large JSON strings in chunks, ask for at least 64 bytes, to avoid writing one + // character at a time. + // This value should be between 12 and 512 (inclusive). + // In the worst case, a 4-byte UTF-8 character, that needs to be escaped using the \uXXXX UTF-16 + // format, will need 12 bytes, for the two UTF-16 escaped characters (high/low surrogate pairs). + // Anything larger than 512 is not feasible since it is difficult for embedded devices to have + // such large blocks of contiguous memory available. + _az_MINIMUM_STRING_CHUNK_SIZE = 64, + + // We need 2 bytes for the quotes, potentially one more for the comma to separate items, and one + // more for the colon if writing a property name. Therefore, only a maximum of 10 character + // strings are guaranteed to fit into a single 64 byte chunk, if all 10 needed to be escaped (i.e. + // multiply by 6). 10 * 6 + 4 = 64, and that fits within _az_MINIMUM_STRING_CHUNK_SIZE + _az_MAX_UNESCAPED_STRING_SIZE_PER_CHUNK = 10, + + // The number of unique values in base 16 (hexadecimal). + _az_NUMBER_OF_HEX_VALUES = 16, +}; + +typedef enum +{ + _az_JSON_STACK_OBJECT = 1, + _az_JSON_STACK_ARRAY = 0, +} _az_json_stack_item; + +AZ_INLINE _az_json_stack_item _az_json_stack_pop(_az_json_bit_stack* ref_json_stack) +{ + _az_PRECONDITION( + ref_json_stack->_internal.current_depth > 0 + && ref_json_stack->_internal.current_depth <= _az_MAX_JSON_STACK_SIZE); + + // Don't do the right bit shift if we are at the last bit in the stack. + if (ref_json_stack->_internal.current_depth != 0) + { + ref_json_stack->_internal.az_json_stack >>= 1U; + + // We don't want current_depth to become negative, in case preconditions are off, and if + // append_container_end is called before append_X_start. + ref_json_stack->_internal.current_depth--; + } + + // true (i.e. 1) means _az_JSON_STACK_OBJECT, while false (i.e. 0) means _az_JSON_STACK_ARRAY + return (ref_json_stack->_internal.az_json_stack & 1U) != 0 ? _az_JSON_STACK_OBJECT + : _az_JSON_STACK_ARRAY; +} + +AZ_INLINE void _az_json_stack_push(_az_json_bit_stack* ref_json_stack, _az_json_stack_item item) +{ + _az_PRECONDITION( + ref_json_stack->_internal.current_depth >= 0 + && ref_json_stack->_internal.current_depth < _az_MAX_JSON_STACK_SIZE); + + ref_json_stack->_internal.current_depth++; + ref_json_stack->_internal.az_json_stack <<= 1U; + ref_json_stack->_internal.az_json_stack |= (uint32_t)item; +} + +AZ_NODISCARD AZ_INLINE _az_json_stack_item _az_json_stack_peek(_az_json_bit_stack const* json_stack) +{ + _az_PRECONDITION( + json_stack->_internal.current_depth >= 0 + && json_stack->_internal.current_depth <= _az_MAX_JSON_STACK_SIZE); + + // true (i.e. 1) means _az_JSON_STACK_OBJECT, while false (i.e. 0) means _az_JSON_STACK_ARRAY + return (json_stack->_internal.az_json_stack & 1U) != 0 ? _az_JSON_STACK_OBJECT + : _az_JSON_STACK_ARRAY; +} + +#include <_az_cfg_suffix.h> + +#endif // _az_SPAN_PRIVATE_H diff --git a/src/az_json_reader.c b/src/az_json_reader.c new file mode 100644 index 00000000..d71ebed4 --- /dev/null +++ b/src/az_json_reader.c @@ -0,0 +1,984 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "az_json_private.h" +#include "az_span_private.h" +#include +#include +#include + +#include + +#include <_az_cfg.h> + +AZ_NODISCARD az_result az_json_reader_init( + az_json_reader* out_json_reader, + az_span json_buffer, + az_json_reader_options const* options) +{ + _az_PRECONDITION(az_span_size(json_buffer) >= 1); + + *out_json_reader = (az_json_reader){ + .token = (az_json_token){ + .kind = AZ_JSON_TOKEN_NONE, + .slice = AZ_SPAN_EMPTY, + .size = 0, + ._internal = { + .is_multisegment = false, + .string_has_escaped_chars = false, + .pointer_to_first_buffer = &AZ_SPAN_EMPTY, + .start_buffer_index = -1, + .start_buffer_offset = -1, + .end_buffer_index = -1, + .end_buffer_offset = -1, + }, + }, + .current_depth = 0, + ._internal = { + .json_buffer = json_buffer, + .json_buffers = &AZ_SPAN_EMPTY, + .number_of_buffers = 1, + .buffer_index = 0, + .bytes_consumed = 0, + .total_bytes_consumed = 0, + .is_complex_json = false, + .bit_stack = { 0 }, + .options = options == NULL ? az_json_reader_options_default() : *options, + }, + }; + return AZ_OK; +} + +AZ_NODISCARD az_result az_json_reader_chunked_init( + az_json_reader* out_json_reader, + az_span json_buffers[], + int32_t number_of_buffers, + az_json_reader_options const* options) +{ + _az_PRECONDITION(number_of_buffers >= 1); + _az_PRECONDITION(az_span_size(json_buffers[0]) >= 1); + + *out_json_reader = (az_json_reader) + { + .token = (az_json_token){ + .kind = AZ_JSON_TOKEN_NONE, + .slice = AZ_SPAN_EMPTY, + .size = 0, + ._internal = { + .is_multisegment = false, + .string_has_escaped_chars = false, + .pointer_to_first_buffer = json_buffers, + .start_buffer_index = -1, + .start_buffer_offset = -1, + .end_buffer_index = -1, + .end_buffer_offset = -1, + }, + }, + .current_depth = 0, + ._internal = { + .json_buffer = json_buffers[0], + .json_buffers = json_buffers, + .number_of_buffers = number_of_buffers, + .buffer_index = 0, + .bytes_consumed = 0, + .total_bytes_consumed = 0, + .is_complex_json = false, + .bit_stack = { 0 }, + .options = options == NULL ? az_json_reader_options_default() : *options, + }, + }; + return AZ_OK; +} + +AZ_NODISCARD static az_span _get_remaining_json(az_json_reader* json_reader) +{ + _az_PRECONDITION_NOT_NULL(json_reader); + + return az_span_slice_to_end( + json_reader->_internal.json_buffer, json_reader->_internal.bytes_consumed); +} + +static void _az_json_reader_update_state( + az_json_reader* ref_json_reader, + az_json_token_kind token_kind, + az_span token_slice, + int32_t current_segment_consumed, + int32_t consumed) +{ + ref_json_reader->token.kind = token_kind; + ref_json_reader->token.size = consumed; + ref_json_reader->current_depth = ref_json_reader->_internal.bit_stack._internal.current_depth; + + // The depth of the start of the container will be one less than the bit stack managing the state. + // That is because we push on the stack when we see a start of the container (above in the call + // stack), but its actual depth and "indentation" level is one lower. + if (token_kind == AZ_JSON_TOKEN_BEGIN_ARRAY || token_kind == AZ_JSON_TOKEN_BEGIN_OBJECT) + { + ref_json_reader->current_depth--; + } + + ref_json_reader->_internal.bytes_consumed += current_segment_consumed; + ref_json_reader->_internal.total_bytes_consumed += consumed; + + // We should have already set start_buffer_index and offset before moving to the next buffer. + ref_json_reader->token._internal.end_buffer_index = ref_json_reader->_internal.buffer_index; + ref_json_reader->token._internal.end_buffer_offset = ref_json_reader->_internal.bytes_consumed; + + ref_json_reader->token._internal.is_multisegment = false; + + // Token straddles more than one segment + int32_t start_index = ref_json_reader->token._internal.start_buffer_index; + if (start_index != -1 && start_index < ref_json_reader->token._internal.end_buffer_index) + { + ref_json_reader->token._internal.is_multisegment = true; + } + + ref_json_reader->token.slice = token_slice; +} + +AZ_NODISCARD static az_result _az_json_reader_get_next_buffer( + az_json_reader* ref_json_reader, + az_span* remaining, + bool skip_whitespace) +{ + // If we only had one buffer, or we ran out of the set of discontiguous buffers, return error. + if (ref_json_reader->_internal.buffer_index >= ref_json_reader->_internal.number_of_buffers - 1) + { + return AZ_ERROR_UNEXPECTED_END; + } + + if (!skip_whitespace && ref_json_reader->token._internal.start_buffer_index == -1) + { + ref_json_reader->token._internal.start_buffer_index = ref_json_reader->_internal.buffer_index; + + ref_json_reader->token._internal.start_buffer_offset + = ref_json_reader->_internal.bytes_consumed; + } + + ref_json_reader->_internal.buffer_index++; + + ref_json_reader->_internal.json_buffer + = ref_json_reader->_internal.json_buffers[ref_json_reader->_internal.buffer_index]; + + ref_json_reader->_internal.bytes_consumed = 0; + + az_span place_holder = _get_remaining_json(ref_json_reader); + + // Found an empty segment in the json_buffers array, which isn't allowed. + if (az_span_size(place_holder) < 1) + { + return AZ_ERROR_UNEXPECTED_END; + } + + *remaining = place_holder; + return AZ_OK; +} + +AZ_NODISCARD static az_span _az_json_reader_skip_whitespace(az_json_reader* ref_json_reader) +{ + az_span json; + az_span remaining = _get_remaining_json(ref_json_reader); + + while (true) + { + json = _az_span_trim_whitespace_from_start(remaining); + + // Find out how many whitespace characters were trimmed. + int32_t consumed = _az_span_diff(json, remaining); + + ref_json_reader->_internal.bytes_consumed += consumed; + ref_json_reader->_internal.total_bytes_consumed += consumed; + + if (az_span_size(json) >= 1 + || az_result_failed(_az_json_reader_get_next_buffer(ref_json_reader, &remaining, true))) + { + break; + } + } + + return json; +} + +AZ_NODISCARD static az_result _az_json_reader_process_container_end( + az_json_reader* ref_json_reader, + az_json_token_kind token_kind) +{ + // The JSON payload is invalid if it has a mismatched container end without a matching open. + if ((token_kind == AZ_JSON_TOKEN_END_OBJECT + && _az_json_stack_peek(&ref_json_reader->_internal.bit_stack) != _az_JSON_STACK_OBJECT) + || (token_kind == AZ_JSON_TOKEN_END_ARRAY + && _az_json_stack_peek(&ref_json_reader->_internal.bit_stack) != _az_JSON_STACK_ARRAY)) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + az_span token = _get_remaining_json(ref_json_reader); + _az_json_stack_pop(&ref_json_reader->_internal.bit_stack); + _az_json_reader_update_state(ref_json_reader, token_kind, az_span_slice(token, 0, 1), 1, 1); + return AZ_OK; +} + +AZ_NODISCARD static az_result _az_json_reader_process_container_start( + az_json_reader* ref_json_reader, + az_json_token_kind token_kind, + _az_json_stack_item container_kind) +{ + // The current depth is equal to or larger than the maximum allowed depth of 64. Cannot read the + // next JSON object or array. + if (ref_json_reader->_internal.bit_stack._internal.current_depth >= _az_MAX_JSON_STACK_SIZE) + { + return AZ_ERROR_JSON_NESTING_OVERFLOW; + } + + az_span token = _get_remaining_json(ref_json_reader); + + _az_json_stack_push(&ref_json_reader->_internal.bit_stack, container_kind); + _az_json_reader_update_state(ref_json_reader, token_kind, az_span_slice(token, 0, 1), 1, 1); + return AZ_OK; +} + +AZ_NODISCARD static bool _az_is_valid_escaped_character(uint8_t byte) +{ + switch (byte) + { + case '\\': + case '"': + case '/': + case 'b': + case 'f': + case 'n': + case 'r': + case 't': + return true; + default: + return false; + } +} + +AZ_NODISCARD static az_result _az_json_reader_process_string(az_json_reader* ref_json_reader) +{ + // Move past the first '"' character + ref_json_reader->_internal.bytes_consumed++; + + az_span token = _get_remaining_json(ref_json_reader); + int32_t remaining_size = az_span_size(token); + + if (remaining_size < 1) + { + _az_RETURN_IF_FAILED(_az_json_reader_get_next_buffer(ref_json_reader, &token, false)); + remaining_size = az_span_size(token); + } + + int32_t current_index = 0; + int32_t string_length = 0; + uint8_t* token_ptr = az_span_ptr(token); + uint8_t next_byte = token_ptr[0]; + + // Clear the state of any previous string token. + ref_json_reader->token._internal.string_has_escaped_chars = false; + + while (true) + { + if (next_byte == '"') + { + break; + } + + if (next_byte == '\\') + { + ref_json_reader->token._internal.string_has_escaped_chars = true; + current_index++; + string_length++; + if (current_index >= remaining_size) + { + _az_RETURN_IF_FAILED(_az_json_reader_get_next_buffer(ref_json_reader, &token, false)); + current_index = 0; + token_ptr = az_span_ptr(token); + remaining_size = az_span_size(token); + } + next_byte = token_ptr[current_index]; + + if (next_byte == 'u') + { + current_index++; + string_length++; + + // Expecting 4 hex digits to follow the escaped 'u' + for (int32_t i = 0; i < 4; i++) + { + if (current_index >= remaining_size) + { + _az_RETURN_IF_FAILED(_az_json_reader_get_next_buffer(ref_json_reader, &token, false)); + current_index = 0; + token_ptr = az_span_ptr(token); + remaining_size = az_span_size(token); + } + + string_length++; + next_byte = token_ptr[current_index++]; + + if (!isxdigit(next_byte)) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + } + + // We have already skipped past the u and 4 hex digits. The loop accounts for incrementing + // by 1 more, so subtract one to account for that. + current_index--; + string_length--; + } + else + { + if (!_az_is_valid_escaped_character(next_byte)) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + } + } + else + { + // Control characters are invalid within a JSON string and should be correctly escaped. + if (next_byte < _az_ASCII_SPACE_CHARACTER) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + } + + current_index++; + string_length++; + + if (current_index >= remaining_size) + { + _az_RETURN_IF_FAILED(_az_json_reader_get_next_buffer(ref_json_reader, &token, false)); + current_index = 0; + token_ptr = az_span_ptr(token); + remaining_size = az_span_size(token); + } + next_byte = token_ptr[current_index]; + } + + _az_json_reader_update_state( + ref_json_reader, + AZ_JSON_TOKEN_STRING, + az_span_slice(token, 0, current_index), + current_index, + string_length); + + // Add 1 to number of bytes consumed to account for the last '"' character. + ref_json_reader->_internal.bytes_consumed++; + ref_json_reader->_internal.total_bytes_consumed++; + + return AZ_OK; +} + +AZ_NODISCARD static az_result _az_json_reader_process_property_name(az_json_reader* ref_json_reader) +{ + _az_RETURN_IF_FAILED(_az_json_reader_process_string(ref_json_reader)); + + az_span json = _az_json_reader_skip_whitespace(ref_json_reader); + + // Expected a colon to indicate that a value will follow after the property name, but instead + // either reached end of data or some other character, which is invalid. + if (az_span_size(json) < 1) + { + return AZ_ERROR_UNEXPECTED_END; + } + if (az_span_ptr(json)[0] != ':') + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + // We don't need to set the json_reader->token.slice since that was already done + // in _az_json_reader_process_string when processing the string portion of the property name. + // Therefore, we don't call _az_json_reader_update_state here. + ref_json_reader->token.kind = AZ_JSON_TOKEN_PROPERTY_NAME; + ref_json_reader->_internal.bytes_consumed++; // For the name / value separator + ref_json_reader->_internal.total_bytes_consumed++; // For the name / value separator + + return AZ_OK; +} + +// Used to search for possible valid end of a number character, when we have complex JSON payloads +// (i.e. not a single JSON value). +// Whitespace characters, comma, or a container end character indicate the end of a JSON number. +static const az_span json_delimiters = AZ_SPAN_LITERAL_FROM_STR(",}] \n\r\t"); + +AZ_NODISCARD static bool _az_finished_consuming_json_number( + uint8_t next_byte, + az_span expected_next_bytes, + az_result* out_result) +{ + az_span next_byte_span = az_span_create(&next_byte, 1); + + // Checking if we are done processing a JSON number + int32_t index = az_span_find(json_delimiters, next_byte_span); + if (index != -1) + { + *out_result = AZ_OK; + return true; + } + + // The next character after a "0" or a set of digits must either be a decimal or 'e'/'E' to + // indicate scientific notation. For example "01" or "123f" is invalid. + // The next character after "[-][digits].[digits]" must be 'e'/'E' if we haven't reached the end + // of the number yet. For example, "1.1f" or "1.1-" are invalid. + index = az_span_find(expected_next_bytes, next_byte_span); + if (index == -1) + { + *out_result = AZ_ERROR_UNEXPECTED_CHAR; + return true; + } + + return false; +} + +static void _az_json_reader_consume_digits( + az_json_reader* ref_json_reader, + az_span* token, + int32_t* current_consumed, + int32_t* total_consumed) +{ + int32_t counter = 0; + az_span current = az_span_slice_to_end(*token, *current_consumed); + while (true) + { + int32_t const token_size = az_span_size(current); + uint8_t* next_byte_ptr = az_span_ptr(current); + + while (counter < token_size) + { + if (isdigit(*next_byte_ptr)) + { + counter++; + next_byte_ptr++; + } + else + { + break; + } + } + if (counter == token_size + && az_result_succeeded(_az_json_reader_get_next_buffer(ref_json_reader, token, false))) + { + *total_consumed += counter; + counter = 0; + *current_consumed = 0; + current = *token; + continue; + } + break; + } + + *total_consumed += counter; + *current_consumed += counter; +} + +AZ_NODISCARD static az_result _az_json_reader_update_number_state_if_single_value( + az_json_reader* ref_json_reader, + az_span token_slice, + int32_t current_consumed, + int32_t total_consumed) +{ + if (ref_json_reader->_internal.is_complex_json) + { + return AZ_ERROR_UNEXPECTED_END; + } + + _az_json_reader_update_state( + ref_json_reader, AZ_JSON_TOKEN_NUMBER, token_slice, current_consumed, total_consumed); + + return AZ_OK; +} + +AZ_NODISCARD static az_result _az_validate_next_byte_is_digit( + az_json_reader* ref_json_reader, + az_span* remaining_number, + int32_t* current_consumed) +{ + az_span current = az_span_slice_to_end(*remaining_number, *current_consumed); + if (az_span_size(current) < 1) + { + _az_RETURN_IF_FAILED(_az_json_reader_get_next_buffer(ref_json_reader, remaining_number, false)); + current = *remaining_number; + *current_consumed = 0; + } + + if (!isdigit(az_span_ptr(current)[0])) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + return AZ_OK; +} + +AZ_NODISCARD static az_result _az_json_reader_process_number(az_json_reader* ref_json_reader) +{ + az_span token = _get_remaining_json(ref_json_reader); + + int32_t total_consumed = 0; + int32_t current_consumed = 0; + + uint8_t next_byte = az_span_ptr(token)[0]; + if (next_byte == '-') + { + total_consumed++; + current_consumed++; + + // A negative sign must be followed by at least one digit. + _az_RETURN_IF_FAILED( + _az_validate_next_byte_is_digit(ref_json_reader, &token, ¤t_consumed)); + + next_byte = az_span_ptr(token)[current_consumed]; + } + + if (next_byte == '0') + { + total_consumed++; + current_consumed++; + + if (current_consumed >= az_span_size(token)) + { + if (az_result_failed(_az_json_reader_get_next_buffer(ref_json_reader, &token, false))) + { + // If there is no more JSON, this is a valid end state only when the JSON payload contains a + // single value: "[-]0" + // Otherwise, the payload is incomplete and ending too early. + return _az_json_reader_update_number_state_if_single_value( + ref_json_reader, + az_span_slice(token, 0, current_consumed), + current_consumed, + total_consumed); + } + current_consumed = 0; + } + + next_byte = az_span_ptr(token)[current_consumed]; + az_result result = AZ_OK; + if (_az_finished_consuming_json_number(next_byte, AZ_SPAN_FROM_STR(".eE"), &result)) + { + if (az_result_succeeded(result)) + { + _az_json_reader_update_state( + ref_json_reader, + AZ_JSON_TOKEN_NUMBER, + az_span_slice(token, 0, current_consumed), + current_consumed, + total_consumed); + } + return result; + } + } + else + { + _az_PRECONDITION(isdigit(next_byte)); + + // Integer part before decimal + _az_json_reader_consume_digits(ref_json_reader, &token, ¤t_consumed, &total_consumed); + + if (current_consumed >= az_span_size(token)) + { + if (az_result_failed(_az_json_reader_get_next_buffer(ref_json_reader, &token, false))) + { + // If there is no more JSON, this is a valid end state only when the JSON payload contains a + // single value: "[-][digits]" + // Otherwise, the payload is incomplete and ending too early. + return _az_json_reader_update_number_state_if_single_value( + ref_json_reader, + az_span_slice(token, 0, current_consumed), + current_consumed, + total_consumed); + } + current_consumed = 0; + } + + next_byte = az_span_ptr(token)[current_consumed]; + az_result result = AZ_OK; + if (_az_finished_consuming_json_number(next_byte, AZ_SPAN_FROM_STR(".eE"), &result)) + { + if (az_result_succeeded(result)) + { + _az_json_reader_update_state( + ref_json_reader, + AZ_JSON_TOKEN_NUMBER, + az_span_slice(token, 0, current_consumed), + current_consumed, + total_consumed); + } + return result; + } + } + + if (next_byte == '.') + { + total_consumed++; + current_consumed++; + + // A decimal point must be followed by at least one digit. + _az_RETURN_IF_FAILED( + _az_validate_next_byte_is_digit(ref_json_reader, &token, ¤t_consumed)); + + // Integer part after decimal + _az_json_reader_consume_digits(ref_json_reader, &token, ¤t_consumed, &total_consumed); + + if (current_consumed >= az_span_size(token)) + { + if (az_result_failed(_az_json_reader_get_next_buffer(ref_json_reader, &token, false))) + { + // If there is no more JSON, this is a valid end state only when the JSON payload contains a + // single value: "[-][digits].[digits]" + // Otherwise, the payload is incomplete and ending too early. + return _az_json_reader_update_number_state_if_single_value( + ref_json_reader, + az_span_slice(token, 0, current_consumed), + current_consumed, + total_consumed); + } + current_consumed = 0; + } + + next_byte = az_span_ptr(token)[current_consumed]; + az_result result = AZ_OK; + if (_az_finished_consuming_json_number(next_byte, AZ_SPAN_FROM_STR("eE"), &result)) + { + if (az_result_succeeded(result)) + { + _az_json_reader_update_state( + ref_json_reader, + AZ_JSON_TOKEN_NUMBER, + az_span_slice(token, 0, current_consumed), + current_consumed, + total_consumed); + } + return result; + } + } + + // Move past 'e'/'E' + total_consumed++; + current_consumed++; + + // The 'e'/'E' character must be followed by a sign or at least one digit. + if (current_consumed >= az_span_size(token)) + { + _az_RETURN_IF_FAILED(_az_json_reader_get_next_buffer(ref_json_reader, &token, false)); + current_consumed = 0; + } + + next_byte = az_span_ptr(token)[current_consumed]; + if (next_byte == '-' || next_byte == '+') + { + total_consumed++; + current_consumed++; + + // A sign must be followed by at least one digit. + _az_RETURN_IF_FAILED( + _az_validate_next_byte_is_digit(ref_json_reader, &token, ¤t_consumed)); + } + + // Integer part after the 'e'/'E' + _az_json_reader_consume_digits(ref_json_reader, &token, ¤t_consumed, &total_consumed); + + if (current_consumed >= az_span_size(token)) + { + if (az_result_failed(_az_json_reader_get_next_buffer(ref_json_reader, &token, false))) + { + + // If there is no more JSON, this is a valid end state only when the JSON payload contains a + // single value: "[-][digits].[digits]e[+|-][digits]" + // Otherwise, the payload is incomplete and ending too early. + return _az_json_reader_update_number_state_if_single_value( + ref_json_reader, + az_span_slice(token, 0, current_consumed), + current_consumed, + total_consumed); + } + current_consumed = 0; + } + + // Checking if we are done processing a JSON number + next_byte = az_span_ptr(token)[current_consumed]; + int32_t index = az_span_find(json_delimiters, az_span_create(&next_byte, 1)); + if (index == -1) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + _az_json_reader_update_state( + ref_json_reader, + AZ_JSON_TOKEN_NUMBER, + az_span_slice(token, 0, current_consumed), + current_consumed, + total_consumed); + + return AZ_OK; +} + +AZ_INLINE int32_t _az_min(int32_t a, int32_t b) { return a < b ? a : b; } + +AZ_NODISCARD static az_result _az_json_reader_process_literal( + az_json_reader* ref_json_reader, + az_span literal, + az_json_token_kind kind) +{ + az_span token = _get_remaining_json(ref_json_reader); + + int32_t const expected_literal_size = az_span_size(literal); + + int32_t already_matched = 0; + + int32_t max_comparable_size = 0; + while (true) + { + int32_t token_size = az_span_size(token); + max_comparable_size = _az_min(token_size, expected_literal_size - already_matched); + + token = az_span_slice(token, 0, max_comparable_size); + + // Return if the subslice that can be compared contains a mismatch. + if (!az_span_is_content_equal( + token, az_span_slice(literal, already_matched, already_matched + max_comparable_size))) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + already_matched += max_comparable_size; + + if (already_matched == expected_literal_size) + { + break; + } + + // If there is no more data, return EOF because the token is smaller than the expected literal. + _az_RETURN_IF_FAILED(_az_json_reader_get_next_buffer(ref_json_reader, &token, false)); + } + + _az_json_reader_update_state( + ref_json_reader, kind, token, max_comparable_size, expected_literal_size); + return AZ_OK; +} + +AZ_NODISCARD static az_result _az_json_reader_process_value( + az_json_reader* ref_json_reader, + uint8_t const next_byte) +{ + if (next_byte == '"') + { + return _az_json_reader_process_string(ref_json_reader); + } + + if (next_byte == '{') + { + return _az_json_reader_process_container_start( + ref_json_reader, AZ_JSON_TOKEN_BEGIN_OBJECT, _az_JSON_STACK_OBJECT); + } + + if (next_byte == '[') + { + return _az_json_reader_process_container_start( + ref_json_reader, AZ_JSON_TOKEN_BEGIN_ARRAY, _az_JSON_STACK_ARRAY); + } + + if (isdigit(next_byte) || next_byte == '-') + { + return _az_json_reader_process_number(ref_json_reader); + } + + if (next_byte == 'f') + { + return _az_json_reader_process_literal( + ref_json_reader, AZ_SPAN_FROM_STR("false"), AZ_JSON_TOKEN_FALSE); + } + + if (next_byte == 't') + { + return _az_json_reader_process_literal( + ref_json_reader, AZ_SPAN_FROM_STR("true"), AZ_JSON_TOKEN_TRUE); + } + + if (next_byte == 'n') + { + return _az_json_reader_process_literal( + ref_json_reader, AZ_SPAN_FROM_STR("null"), AZ_JSON_TOKEN_NULL); + } + + return AZ_ERROR_UNEXPECTED_CHAR; +} + +AZ_NODISCARD static az_result _az_json_reader_read_first_token( + az_json_reader* ref_json_reader, + az_span json, + uint8_t const first_byte) +{ + if (first_byte == '{') + { + _az_json_stack_push(&ref_json_reader->_internal.bit_stack, _az_JSON_STACK_OBJECT); + + _az_json_reader_update_state( + ref_json_reader, AZ_JSON_TOKEN_BEGIN_OBJECT, az_span_slice(json, 0, 1), 1, 1); + + ref_json_reader->_internal.is_complex_json = true; + return AZ_OK; + } + + if (first_byte == '[') + { + _az_json_stack_push(&ref_json_reader->_internal.bit_stack, _az_JSON_STACK_ARRAY); + + _az_json_reader_update_state( + ref_json_reader, AZ_JSON_TOKEN_BEGIN_ARRAY, az_span_slice(json, 0, 1), 1, 1); + + ref_json_reader->_internal.is_complex_json = true; + return AZ_OK; + } + + return _az_json_reader_process_value(ref_json_reader, first_byte); +} + +AZ_NODISCARD static az_result _az_json_reader_process_next_byte( + az_json_reader* ref_json_reader, + uint8_t next_byte) +{ + // Extra data after a single JSON value (complete object or array or one primitive value) is + // invalid. Expected end of data. + if (ref_json_reader->_internal.bit_stack._internal.current_depth == 0) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + bool within_object + = _az_json_stack_peek(&ref_json_reader->_internal.bit_stack) == _az_JSON_STACK_OBJECT; + + if (next_byte == ',') + { + ref_json_reader->_internal.bytes_consumed++; + + az_span json = _az_json_reader_skip_whitespace(ref_json_reader); + + // Expected start of a property name or value, but instead reached end of data. + if (az_span_size(json) < 1) + { + return AZ_ERROR_UNEXPECTED_END; + } + + next_byte = az_span_ptr(json)[0]; + + if (within_object) + { + // Expected start of a property name after the comma since we are within a JSON object. + if (next_byte != '"') + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + return _az_json_reader_process_property_name(ref_json_reader); + } + + return _az_json_reader_process_value(ref_json_reader, next_byte); + } + + if (next_byte == '}') + { + return _az_json_reader_process_container_end(ref_json_reader, AZ_JSON_TOKEN_END_OBJECT); + } + + if (next_byte == ']') + { + return _az_json_reader_process_container_end(ref_json_reader, AZ_JSON_TOKEN_END_ARRAY); + } + + // No other character is a valid token delimiter within JSON. + return AZ_ERROR_UNEXPECTED_CHAR; +} + +AZ_NODISCARD az_result az_json_reader_next_token(az_json_reader* ref_json_reader) +{ + _az_PRECONDITION_NOT_NULL(ref_json_reader); + + az_span json = _az_json_reader_skip_whitespace(ref_json_reader); + + if (az_span_size(json) < 1) + { + if (ref_json_reader->token.kind == AZ_JSON_TOKEN_NONE + || ref_json_reader->_internal.bit_stack._internal.current_depth != 0) + { + // An empty JSON payload is invalid. + return AZ_ERROR_UNEXPECTED_END; + } + + // No more JSON text left to process, we are done. + return AZ_ERROR_JSON_READER_DONE; + } + + // Clear the internal state of any previous token. + ref_json_reader->token._internal.start_buffer_index = -1; + ref_json_reader->token._internal.start_buffer_offset = -1; + ref_json_reader->token._internal.end_buffer_index = -1; + ref_json_reader->token._internal.end_buffer_offset = -1; + + uint8_t const first_byte = az_span_ptr(json)[0]; + + switch (ref_json_reader->token.kind) + { + case AZ_JSON_TOKEN_NONE: + { + return _az_json_reader_read_first_token(ref_json_reader, json, first_byte); + } + case AZ_JSON_TOKEN_BEGIN_OBJECT: + { + if (first_byte == '}') + { + return _az_json_reader_process_container_end(ref_json_reader, AZ_JSON_TOKEN_END_OBJECT); + } + + // We expect the start of a property name as the first non-whitespace character within a + // JSON object. + if (first_byte != '"') + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + return _az_json_reader_process_property_name(ref_json_reader); + } + case AZ_JSON_TOKEN_BEGIN_ARRAY: + { + if (first_byte == ']') + { + return _az_json_reader_process_container_end(ref_json_reader, AZ_JSON_TOKEN_END_ARRAY); + } + + return _az_json_reader_process_value(ref_json_reader, first_byte); + } + case AZ_JSON_TOKEN_PROPERTY_NAME: + return _az_json_reader_process_value(ref_json_reader, first_byte); + case AZ_JSON_TOKEN_END_OBJECT: + case AZ_JSON_TOKEN_END_ARRAY: + case AZ_JSON_TOKEN_STRING: + case AZ_JSON_TOKEN_NUMBER: + case AZ_JSON_TOKEN_TRUE: + case AZ_JSON_TOKEN_FALSE: + case AZ_JSON_TOKEN_NULL: + return _az_json_reader_process_next_byte(ref_json_reader, first_byte); + default: + return AZ_ERROR_JSON_INVALID_STATE; + } +} + +AZ_NODISCARD az_result az_json_reader_skip_children(az_json_reader* ref_json_reader) +{ + _az_PRECONDITION_NOT_NULL(ref_json_reader); + + if (ref_json_reader->token.kind == AZ_JSON_TOKEN_PROPERTY_NAME) + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + } + + az_json_token_kind const token_kind = ref_json_reader->token.kind; + if (token_kind == AZ_JSON_TOKEN_BEGIN_OBJECT || token_kind == AZ_JSON_TOKEN_BEGIN_ARRAY) + { + // Keep moving the reader until we come back to the same depth. + int32_t const depth = ref_json_reader->_internal.bit_stack._internal.current_depth; + do + { + _az_RETURN_IF_FAILED(az_json_reader_next_token(ref_json_reader)); + } while (depth <= ref_json_reader->_internal.bit_stack._internal.current_depth); + } + return AZ_OK; +} diff --git a/src/az_json_token.c b/src/az_json_token.c new file mode 100644 index 00000000..6d443add --- /dev/null +++ b/src/az_json_token.c @@ -0,0 +1,601 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include +#include +#include + +#include "az_json_private.h" + +#include "az_span_private.h" +#include <_az_cfg.h> + +static az_span _az_json_token_copy_into_span_helper( + az_json_token const* json_token, + az_span destination) +{ + _az_PRECONDITION(json_token->_internal.is_multisegment); + + if (json_token->size == 0) + { + return destination; + } + + for (int32_t i = json_token->_internal.start_buffer_index; + i <= json_token->_internal.end_buffer_index; + i++) + { + az_span source = json_token->_internal.pointer_to_first_buffer[i]; + if (i == json_token->_internal.start_buffer_index) + { + source = az_span_slice_to_end(source, json_token->_internal.start_buffer_offset); + } + else if (i == json_token->_internal.end_buffer_index) + { + source = az_span_slice(source, 0, json_token->_internal.end_buffer_offset); + } + destination = az_span_copy(destination, source); + } + + return destination; +} + +az_span az_json_token_copy_into_span(az_json_token const* json_token, az_span destination) +{ + _az_PRECONDITION_VALID_SPAN(destination, json_token->size, false); + + az_span token_slice = json_token->slice; + + // Contiguous token + if (!json_token->_internal.is_multisegment) + { + return az_span_copy(destination, token_slice); + } + + // Token straddles more than one segment + return _az_json_token_copy_into_span_helper(json_token, destination); +} + +AZ_NODISCARD static uint8_t _az_json_unescape_single_byte(uint8_t ch) +{ + switch (ch) + { + case 'b': + return '\b'; + case 'f': + return '\f'; + case 'n': + return '\n'; + case 'r': + return '\r'; + case 't': + return '\t'; + case '\\': + case '"': + case '/': + default: + { + // We are assuming the JSON token string has already been validated before this and we won't + // have unexpected bytes folowing the back slash (for example \q). Therefore, just return the + // same character back for such cases. + return ch; + } + } +} + +AZ_NODISCARD static bool _az_json_token_is_text_equal_helper( + az_span token_slice, + az_span* expected_text, + bool* next_char_escaped) +{ + int32_t token_size = az_span_size(token_slice); + uint8_t* token_ptr = az_span_ptr(token_slice); + + int32_t expected_size = az_span_size(*expected_text); + uint8_t* expected_ptr = az_span_ptr(*expected_text); + + int32_t token_idx = 0; + for (int32_t i = 0; i < expected_size; i++) + { + if (token_idx >= token_size) + { + *expected_text = az_span_slice_to_end(*expected_text, i); + return false; + } + uint8_t token_byte = token_ptr[token_idx]; + + if (token_byte == '\\' || *next_char_escaped) + { + if (*next_char_escaped) + { + token_byte = _az_json_unescape_single_byte(token_byte); + } + else + { + token_idx++; + if (token_idx >= token_size) + { + *next_char_escaped = true; + *expected_text = az_span_slice_to_end(*expected_text, i); + return false; + } + token_byte = _az_json_unescape_single_byte(token_ptr[token_idx]); + } + *next_char_escaped = false; + + // TODO: Characters escaped in the form of \uXXXX where XXXX is the UTF-16 code point, is + // not currently supported. + // To do this, we need to encode UTF-16 codepoints (including surrogate pairs) into UTF-8. + if (token_byte == 'u') + { + *expected_text = AZ_SPAN_EMPTY; + return false; + } + } + + if (token_byte != expected_ptr[i]) + { + *expected_text = AZ_SPAN_EMPTY; + return false; + } + + token_idx++; + } + + *expected_text = AZ_SPAN_EMPTY; + + // Only return true if the size of the unescaped token matches the expected size exactly. + return token_idx == token_size; +} + +AZ_NODISCARD bool az_json_token_is_text_equal( + az_json_token const* json_token, + az_span expected_text) +{ + _az_PRECONDITION_NOT_NULL(json_token); + + // Cannot compare the value of non-string token kinds + if (json_token->kind != AZ_JSON_TOKEN_STRING && json_token->kind != AZ_JSON_TOKEN_PROPERTY_NAME) + { + return false; + } + + az_span token_slice = json_token->slice; + + // There is nothing to unescape here, compare directly. + if (!json_token->_internal.string_has_escaped_chars) + { + // Contiguous token + if (!json_token->_internal.is_multisegment) + { + return az_span_is_content_equal(token_slice, expected_text); + } + + // Token straddles more than one segment + for (int32_t i = json_token->_internal.start_buffer_index; + i <= json_token->_internal.end_buffer_index; + i++) + { + az_span source = json_token->_internal.pointer_to_first_buffer[i]; + if (i == json_token->_internal.start_buffer_index) + { + source = az_span_slice_to_end(source, json_token->_internal.start_buffer_offset); + } + else if (i == json_token->_internal.end_buffer_index) + { + source = az_span_slice(source, 0, json_token->_internal.end_buffer_offset); + } + + int32_t source_size = az_span_size(source); + if (az_span_size(expected_text) < source_size + || !az_span_is_content_equal(source, az_span_slice(expected_text, 0, source_size))) + { + return false; + } + expected_text = az_span_slice_to_end(expected_text, source_size); + } + // Only return true if we have gone through and compared the entire expected_text. + return az_span_size(expected_text) == 0; + } + + int32_t token_size = json_token->size; + int32_t expected_size = az_span_size(expected_text); + + // No need to try to unescape the token slice, since the lengths won't match anyway. + // Unescaping always shrinks the string, at most by a factor of 6. + if (token_size < expected_size + || (token_size / _az_MAX_EXPANSION_FACTOR_WHILE_ESCAPING) > expected_size) + { + return false; + } + + bool next_char_escaped = false; + + // Contiguous token + if (!json_token->_internal.is_multisegment) + { + return _az_json_token_is_text_equal_helper(token_slice, &expected_text, &next_char_escaped); + } + + // Token straddles more than one segment + for (int32_t i = json_token->_internal.start_buffer_index; + i <= json_token->_internal.end_buffer_index; + i++) + { + az_span source = json_token->_internal.pointer_to_first_buffer[i]; + if (i == json_token->_internal.start_buffer_index) + { + source = az_span_slice_to_end(source, json_token->_internal.start_buffer_offset); + } + else if (i == json_token->_internal.end_buffer_index) + { + source = az_span_slice(source, 0, json_token->_internal.end_buffer_offset); + } + + if (!_az_json_token_is_text_equal_helper(source, &expected_text, &next_char_escaped) + && az_span_size(expected_text) == 0) + { + return false; + } + } + + // Only return true if we have gone through and compared the entire expected_text. + return az_span_size(expected_text) == 0; +} + +AZ_NODISCARD az_result az_json_token_get_boolean(az_json_token const* json_token, bool* out_value) +{ + _az_PRECONDITION_NOT_NULL(json_token); + _az_PRECONDITION_NOT_NULL(out_value); + + if (json_token->kind != AZ_JSON_TOKEN_TRUE && json_token->kind != AZ_JSON_TOKEN_FALSE) + { + return AZ_ERROR_JSON_INVALID_STATE; + } + + // We assume the az_json_token is well-formed and self-consistent when returned from the + // az_json_reader and that if json_token->kind == AZ_JSON_TOKEN_TRUE, then the slice contains the + // characters "true", otherwise it contains "false". Therefore, there is no need to check the + // contents again. + + az_span token_slice = json_token->slice; + + // Contiguous token + if (!json_token->_internal.is_multisegment) + { + *out_value = az_span_size(token_slice) == _az_STRING_LITERAL_LEN("true"); + } + else + { + // Token straddles more than one segment + *out_value = json_token->size == _az_STRING_LITERAL_LEN("true"); + } + + return AZ_OK; +} + +AZ_NODISCARD static az_result _az_json_token_get_string_helper( + az_span source, + char* destination, + int32_t destination_max_size, + int32_t* dest_idx, + bool* next_char_escaped) +{ + int32_t source_size = az_span_size(source); + uint8_t* source_ptr = az_span_ptr(source); + for (int32_t i = 0; i < source_size; i++) + { + if (*dest_idx >= destination_max_size) + { + return AZ_ERROR_NOT_ENOUGH_SPACE; + } + uint8_t token_byte = source_ptr[i]; + + if (token_byte == '\\' || *next_char_escaped) + { + if (*next_char_escaped) + { + token_byte = _az_json_unescape_single_byte(token_byte); + } + else + { + i++; + if (i >= source_size) + { + *next_char_escaped = true; + break; + } + token_byte = _az_json_unescape_single_byte(source_ptr[i]); + } + *next_char_escaped = false; + + // TODO: Characters escaped in the form of \uXXXX where XXXX is the UTF-16 code point, is + // not currently supported. + // To do this, we need to encode UTF-16 codepoints (including surrogate pairs) into UTF-8. + if (token_byte == 'u') + { + return AZ_ERROR_NOT_IMPLEMENTED; + } + } + + destination[*dest_idx] = (char)token_byte; + *dest_idx = *dest_idx + 1; + } + + return AZ_OK; +} + +AZ_NODISCARD az_result az_json_token_get_string( + az_json_token const* json_token, + char* destination, + int32_t destination_max_size, + int32_t* out_string_length) +{ + _az_PRECONDITION_NOT_NULL(json_token); + _az_PRECONDITION_NOT_NULL(destination); + _az_PRECONDITION(destination_max_size > 0); + + if (json_token->kind != AZ_JSON_TOKEN_STRING && json_token->kind != AZ_JSON_TOKEN_PROPERTY_NAME) + { + return AZ_ERROR_JSON_INVALID_STATE; + } + + az_span token_slice = json_token->slice; + int32_t token_size = json_token->size; + + // There is nothing to unescape here, copy directly. + if (!json_token->_internal.string_has_escaped_chars) + { + // We need enough space to add a null terminator. + if (token_size >= destination_max_size) + { + return AZ_ERROR_NOT_ENOUGH_SPACE; + } + + // Contiguous token + if (!json_token->_internal.is_multisegment) + { + // This will add a null terminator. + az_span_to_str(destination, destination_max_size, token_slice); + } + else + { + // Token straddles more than one segment + az_span remainder = _az_json_token_copy_into_span_helper( + json_token, az_span_create((uint8_t*)destination, destination_max_size)); + + // Add a null terminator. + az_span_copy_u8(remainder, 0); + } + + if (out_string_length != NULL) + { + *out_string_length = token_size; + } + return AZ_OK; + } + + // No need to try to unescape the token slice, if the destination is known to be too small. + // Unescaping always shrinks the string, at most by a factor of 6. + // We also need enough space to add a null terminator. + if (token_size / _az_MAX_EXPANSION_FACTOR_WHILE_ESCAPING >= destination_max_size) + { + return AZ_ERROR_NOT_ENOUGH_SPACE; + } + + int32_t dest_idx = 0; + bool next_char_escaped = false; + + // Contiguous token + if (!json_token->_internal.is_multisegment) + { + _az_RETURN_IF_FAILED(_az_json_token_get_string_helper( + token_slice, destination, destination_max_size, &dest_idx, &next_char_escaped)); + } + else + { + // Token straddles more than one segment + for (int32_t i = json_token->_internal.start_buffer_index; + i <= json_token->_internal.end_buffer_index; + i++) + { + az_span source = json_token->_internal.pointer_to_first_buffer[i]; + if (i == json_token->_internal.start_buffer_index) + { + source = az_span_slice_to_end(source, json_token->_internal.start_buffer_offset); + } + else if (i == json_token->_internal.end_buffer_index) + { + source = az_span_slice(source, 0, json_token->_internal.end_buffer_offset); + } + + _az_RETURN_IF_FAILED(_az_json_token_get_string_helper( + source, destination, destination_max_size, &dest_idx, &next_char_escaped)); + } + } + + if (dest_idx >= destination_max_size) + { + return AZ_ERROR_NOT_ENOUGH_SPACE; + } + destination[dest_idx] = 0; + + if (out_string_length != NULL) + { + *out_string_length = dest_idx; + } + + return AZ_OK; +} + +AZ_NODISCARD az_result +az_json_token_get_uint64(az_json_token const* json_token, uint64_t* out_value) +{ + _az_PRECONDITION_NOT_NULL(json_token); + _az_PRECONDITION_NOT_NULL(out_value); + + if (json_token->kind != AZ_JSON_TOKEN_NUMBER) + { + return AZ_ERROR_JSON_INVALID_STATE; + } + + az_span token_slice = json_token->slice; + + // Contiguous token + if (!json_token->_internal.is_multisegment) + { + return az_span_atou64(token_slice, out_value); + } + + // Any number that won't fit in the scratch buffer, will overflow. + if (json_token->size > _az_MAX_SIZE_FOR_UINT64) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + // Token straddles more than one segment. + // Used to copy discontiguous token values into a contiguous buffer, for number parsing. + uint8_t scratch_buffer[_az_MAX_SIZE_FOR_UINT64] = { 0 }; + az_span scratch = AZ_SPAN_FROM_BUFFER(scratch_buffer); + + az_span remainder = _az_json_token_copy_into_span_helper(json_token, scratch); + + return az_span_atou64(az_span_slice(scratch, 0, _az_span_diff(remainder, scratch)), out_value); +} + +AZ_NODISCARD az_result +az_json_token_get_uint32(az_json_token const* json_token, uint32_t* out_value) +{ + _az_PRECONDITION_NOT_NULL(json_token); + _az_PRECONDITION_NOT_NULL(out_value); + + if (json_token->kind != AZ_JSON_TOKEN_NUMBER) + { + return AZ_ERROR_JSON_INVALID_STATE; + } + + az_span token_slice = json_token->slice; + + // Contiguous token + if (!json_token->_internal.is_multisegment) + { + return az_span_atou32(token_slice, out_value); + } + + // Any number that won't fit in the scratch buffer, will overflow. + if (json_token->size > _az_MAX_SIZE_FOR_UINT32) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + // Token straddles more than one segment. + // Used to copy discontiguous token values into a contiguous buffer, for number parsing. + uint8_t scratch_buffer[_az_MAX_SIZE_FOR_UINT32] = { 0 }; + az_span scratch = AZ_SPAN_FROM_BUFFER(scratch_buffer); + + az_span remainder = _az_json_token_copy_into_span_helper(json_token, scratch); + + return az_span_atou32(az_span_slice(scratch, 0, _az_span_diff(remainder, scratch)), out_value); +} + +AZ_NODISCARD az_result az_json_token_get_int64(az_json_token const* json_token, int64_t* out_value) +{ + _az_PRECONDITION_NOT_NULL(json_token); + _az_PRECONDITION_NOT_NULL(out_value); + + if (json_token->kind != AZ_JSON_TOKEN_NUMBER) + { + return AZ_ERROR_JSON_INVALID_STATE; + } + + az_span token_slice = json_token->slice; + + // Contiguous token + if (!json_token->_internal.is_multisegment) + { + return az_span_atoi64(token_slice, out_value); + } + + // Any number that won't fit in the scratch buffer, will overflow. + if (json_token->size > _az_MAX_SIZE_FOR_INT64) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + // Token straddles more than one segment. + // Used to copy discontiguous token values into a contiguous buffer, for number parsing. + uint8_t scratch_buffer[_az_MAX_SIZE_FOR_INT64] = { 0 }; + az_span scratch = AZ_SPAN_FROM_BUFFER(scratch_buffer); + + az_span remainder = _az_json_token_copy_into_span_helper(json_token, scratch); + + return az_span_atoi64(az_span_slice(scratch, 0, _az_span_diff(remainder, scratch)), out_value); +} + +AZ_NODISCARD az_result az_json_token_get_int32(az_json_token const* json_token, int32_t* out_value) +{ + _az_PRECONDITION_NOT_NULL(json_token); + _az_PRECONDITION_NOT_NULL(out_value); + + if (json_token->kind != AZ_JSON_TOKEN_NUMBER) + { + return AZ_ERROR_JSON_INVALID_STATE; + } + + az_span token_slice = json_token->slice; + + // Contiguous token + if (!json_token->_internal.is_multisegment) + { + return az_span_atoi32(token_slice, out_value); + } + + // Any number that won't fit in the scratch buffer, will overflow. + if (json_token->size > _az_MAX_SIZE_FOR_INT32) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + // Token straddles more than one segment. + // Used to copy discontiguous token values into a contiguous buffer, for number parsing. + uint8_t scratch_buffer[_az_MAX_SIZE_FOR_INT32] = { 0 }; + az_span scratch = AZ_SPAN_FROM_BUFFER(scratch_buffer); + + az_span remainder = _az_json_token_copy_into_span_helper(json_token, scratch); + + return az_span_atoi32(az_span_slice(scratch, 0, _az_span_diff(remainder, scratch)), out_value); +} + +AZ_NODISCARD az_result az_json_token_get_double(az_json_token const* json_token, double* out_value) +{ + _az_PRECONDITION_NOT_NULL(json_token); + _az_PRECONDITION_NOT_NULL(out_value); + + if (json_token->kind != AZ_JSON_TOKEN_NUMBER) + { + return AZ_ERROR_JSON_INVALID_STATE; + } + + az_span token_slice = json_token->slice; + + // Contiguous token + if (!json_token->_internal.is_multisegment) + { + return az_span_atod(token_slice, out_value); + } + + // Any number that won't fit in the scratch buffer, will overflow. + if (json_token->size > _az_MAX_SIZE_FOR_PARSING_DOUBLE) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + // Token straddles more than one segment. + // Used to copy discontiguous token values into a contiguous buffer, for number parsing. + uint8_t scratch_buffer[_az_MAX_SIZE_FOR_PARSING_DOUBLE] = { 0 }; + az_span scratch = AZ_SPAN_FROM_BUFFER(scratch_buffer); + + az_span remainder = _az_json_token_copy_into_span_helper(json_token, scratch); + + return az_span_atod(az_span_slice(scratch, 0, _az_span_diff(remainder, scratch)), out_value); +} diff --git a/src/az_json_writer.c b/src/az_json_writer.c new file mode 100644 index 00000000..98518286 --- /dev/null +++ b/src/az_json_writer.c @@ -0,0 +1,1021 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "az_hex_private.h" +#include "az_json_private.h" +#include "az_span_private.h" +#include +#include +#include + +#include + +#include <_az_cfg.h> + +AZ_NODISCARD az_result az_json_writer_init( + az_json_writer* out_json_writer, + az_span destination_buffer, + az_json_writer_options const* options) +{ + _az_PRECONDITION_NOT_NULL(out_json_writer); + + *out_json_writer = (az_json_writer){ + ._internal = { + .destination_buffer = destination_buffer, + .allocator_callback = NULL, + .user_context = NULL, + .bytes_written = 0, + .total_bytes_written = 0, + .need_comma = false, + .token_kind = AZ_JSON_TOKEN_NONE, + .bit_stack = { 0 }, + .options = options == NULL ? az_json_writer_options_default() : *options, + }, + }; + return AZ_OK; +} + +AZ_NODISCARD az_result az_json_writer_chunked_init( + az_json_writer* out_json_writer, + az_span first_destination_buffer, + az_span_allocator_fn allocator_callback, + void* user_context, + az_json_writer_options const* options) +{ + _az_PRECONDITION_NOT_NULL(out_json_writer); + _az_PRECONDITION_NOT_NULL(allocator_callback); + + *out_json_writer = (az_json_writer){ + ._internal = { + .destination_buffer = first_destination_buffer, + .allocator_callback = allocator_callback, + .user_context = user_context, + .bytes_written = 0, + .total_bytes_written = 0, + .need_comma = false, + .token_kind = AZ_JSON_TOKEN_NONE, + .bit_stack = { 0 }, + .options = options == NULL ? az_json_writer_options_default() : *options, + }, + }; + return AZ_OK; +} + +static AZ_NODISCARD az_span +_get_remaining_span(az_json_writer* ref_json_writer, int32_t required_size) +{ + _az_PRECONDITION_NOT_NULL(ref_json_writer); + _az_PRECONDITION(required_size > 0); + + az_span remaining = az_span_slice_to_end( + ref_json_writer->_internal.destination_buffer, ref_json_writer->_internal.bytes_written); + + if (az_span_size(remaining) < required_size + && ref_json_writer->_internal.allocator_callback != NULL) + { + az_span_allocator_context context = { + .user_context = ref_json_writer->_internal.user_context, + .bytes_used = ref_json_writer->_internal.bytes_written, + .minimum_required_size = required_size, + }; + + // No more space left in the destination, let the caller fail with AZ_ERROR_NOT_ENOUGH_SPACE. + if (az_result_failed(ref_json_writer->_internal.allocator_callback(&context, &remaining))) + { + return AZ_SPAN_EMPTY; + } + ref_json_writer->_internal.destination_buffer = remaining; + ref_json_writer->_internal.bytes_written = 0; + } + + return remaining; +} + +// This validation method is used outside of just preconditions, within +// az_json_writer_append_json_text. +static AZ_NODISCARD bool _az_is_appending_value_valid(az_json_writer const* json_writer) +{ + _az_PRECONDITION_NOT_NULL(json_writer); + + az_json_token_kind kind = json_writer->_internal.token_kind; + + if (_az_json_stack_peek(&json_writer->_internal.bit_stack)) + { + // Cannot write a JSON value within an object without a property name first. + // That includes writing the start of an object or array without a property name. + if (kind != AZ_JSON_TOKEN_PROPERTY_NAME) + { + // Given we are within a JSON object, kind cannot be start of an array or none. + _az_PRECONDITION(kind != AZ_JSON_TOKEN_NONE && kind != AZ_JSON_TOKEN_BEGIN_ARRAY); + + return false; + } + } + else + { + // Adding a JSON value within a JSON array is allowed and it is also allowed to add a standalone + // single JSON value. However, it is invalid to add multiple JSON values that aren't within a + // container, or outside an existing closed object/array. + + // That includes writing the start of an object or array after a single JSON value or outside of + // an existing closed object/array. + + // Given we are not within a JSON object, kind cannot be property name. + _az_PRECONDITION(kind != AZ_JSON_TOKEN_PROPERTY_NAME && kind != AZ_JSON_TOKEN_BEGIN_OBJECT); + + // It is more likely for current_depth to not equal 0 when writing valid JSON, so check that + // first to rely on short-circuiting and return quickly. + if (json_writer->_internal.bit_stack._internal.current_depth == 0 && kind != AZ_JSON_TOKEN_NONE) + { + return false; + } + } + + // JSON writer state is valid and a primitive value or start of a container can be appended. + return true; +} + +#ifndef AZ_NO_PRECONDITION_CHECKING +static AZ_NODISCARD bool _az_is_appending_property_name_valid(az_json_writer const* json_writer) +{ + _az_PRECONDITION_NOT_NULL(json_writer); + + az_json_token_kind kind = json_writer->_internal.token_kind; + + // Cannot write a JSON property within an array or as the first JSON token. + // Cannot write a JSON property name following another property name. A JSON value is missing. + if (!_az_json_stack_peek(&json_writer->_internal.bit_stack) + || kind == AZ_JSON_TOKEN_PROPERTY_NAME) + { + _az_PRECONDITION(kind != AZ_JSON_TOKEN_BEGIN_OBJECT); + return false; + } + + // JSON writer state is valid and a property name can be appended. + return true; +} + +static AZ_NODISCARD bool _az_is_appending_container_end_valid( + az_json_writer const* json_writer, + uint8_t byte) +{ + _az_PRECONDITION_NOT_NULL(json_writer); + + az_json_token_kind kind = json_writer->_internal.token_kind; + + // Cannot write an end of a container without a matching start. + // This includes writing the end token as the first token in the JSON or right after a property + // name. + if (json_writer->_internal.bit_stack._internal.current_depth <= 0 + || kind == AZ_JSON_TOKEN_PROPERTY_NAME) + { + return false; + } + + _az_json_stack_item stack_item = _az_json_stack_peek(&json_writer->_internal.bit_stack); + + if (byte == ']') + { + // If inside a JSON object, then appending an end bracket is invalid: + if (stack_item) + { + _az_PRECONDITION(kind != AZ_JSON_TOKEN_NONE); + return false; + } + } + else + { + _az_PRECONDITION(byte == '}'); + + // If not inside a JSON object, then appending an end brace is invalid: + if (!stack_item) + { + return false; + } + } + + // JSON writer state is valid and an end of a container can be appended. + return true; +} +#endif // AZ_NO_PRECONDITION_CHECKING + +// Returns the length of the JSON string within the az_span after it has been escaped. +// The out parameter contains the index where the first character to escape is found. +// If no chars need to be escaped then return the size of value with the out parameter set to -1. +// If break_on_first_escaped is set to true, then it returns as soon as the first character to +// escape is found. +static int32_t _az_json_writer_escaped_length( + az_span value, + int32_t* out_index_of_first_escaped_char, + bool break_on_first_escaped) +{ + _az_PRECONDITION_NOT_NULL(out_index_of_first_escaped_char); + _az_PRECONDITION_VALID_SPAN(value, 0, true); + + int32_t value_size = az_span_size(value); + _az_PRECONDITION(value_size <= _az_MAX_UNESCAPED_STRING_SIZE); + + int32_t escaped_length = 0; + *out_index_of_first_escaped_char = -1; + + int32_t i = 0; + uint8_t* value_ptr = az_span_ptr(value); + + while (i < value_size) + { + uint8_t const ch = value_ptr[i]; + + switch (ch) + { + case '\\': + case '"': + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + { + escaped_length += 2; // Use the two-character sequence escape for these. + break; + } + default: + { + // Check if the character has to be escaped as a UNICODE escape sequence. + if (ch < _az_ASCII_SPACE_CHARACTER) + { + escaped_length += _az_MAX_EXPANSION_FACTOR_WHILE_ESCAPING; + } + else + { + escaped_length++; // No escaping required. + } + break; + } + } + + i++; + + // If this is the first time that we found a character that needs to be escaped, + // set out_index_of_first_escaped_char to the corresponding index. + // If escaped_length == i, then we haven't found a character that needs to be escaped yet. + if (escaped_length != i && *out_index_of_first_escaped_char == -1) + { + *out_index_of_first_escaped_char = i - 1; + if (break_on_first_escaped) + { + break; + } + } + + // If the length overflows, in case the precondition is not honored, stop processing and break + // The caller will return AZ_ERROR_NOT_ENOUGH_SPACE since az_span can't contain it. + // TODO: Consider removing this if it is too costly. + if (escaped_length < 0) + { + escaped_length = INT32_MAX; + break; + } + } + + // In most common cases, escaped_length will equal value_size and out_index_of_first_escaped_char + // will be -1. + return escaped_length; +} + +static int32_t _az_json_writer_escape_next_byte_and_copy( + az_span* remaining_destination, + uint8_t next_byte) +{ + uint8_t escaped = 0; + int32_t written = 0; + + switch (next_byte) + { + case '\\': + case '"': + { + escaped = next_byte; + break; + } + case '\b': + { + escaped = 'b'; + break; + } + case '\f': + { + escaped = 'f'; + break; + } + case '\n': + { + escaped = 'n'; + break; + } + case '\r': + { + escaped = 'r'; + break; + } + case '\t': + { + escaped = 't'; + break; + } + default: + { + // Check if the character has to be escaped as a UNICODE escape sequence. + if (next_byte < _az_ASCII_SPACE_CHARACTER) + { + // TODO: Consider moving this array outside the loop. + uint8_t array[_az_MAX_EXPANSION_FACTOR_WHILE_ESCAPING] = { + '\\', + 'u', + '0', + '0', + _az_number_to_upper_hex((uint8_t)(next_byte / _az_NUMBER_OF_HEX_VALUES)), + _az_number_to_upper_hex((uint8_t)(next_byte % _az_NUMBER_OF_HEX_VALUES)), + }; + *remaining_destination = az_span_copy(*remaining_destination, AZ_SPAN_FROM_BUFFER(array)); + written += _az_MAX_EXPANSION_FACTOR_WHILE_ESCAPING; + } + else + { + *remaining_destination = az_span_copy_u8(*remaining_destination, next_byte); + written++; + } + break; + } + } + + // If escaped is non-zero, then we found one of the characters that needs to be escaped. + // Otherwise, we hit the default case in the switch above, in which case, we already wrote + // the character. + if (escaped) + { + *remaining_destination = az_span_copy_u8(*remaining_destination, '\\'); + *remaining_destination = az_span_copy_u8(*remaining_destination, escaped); + written += 2; + } + return written; +} + +static AZ_NODISCARD az_span _az_json_writer_escape_and_copy(az_span destination, az_span source) +{ + _az_PRECONDITION_VALID_SPAN(source, 1, false); + + int32_t src_size = az_span_size(source); + _az_PRECONDITION(src_size <= _az_MAX_UNESCAPED_STRING_SIZE); + _az_PRECONDITION_VALID_SPAN(destination, src_size + 1, false); + + int32_t i = 0; + uint8_t* value_ptr = az_span_ptr(source); + + az_span remaining_destination = destination; + + while (i < src_size) + { + uint8_t const ch = value_ptr[i]; + _az_json_writer_escape_next_byte_and_copy(&remaining_destination, ch); + i++; + } + + return remaining_destination; +} + +AZ_INLINE void _az_update_json_writer_state( + az_json_writer* ref_json_writer, + int32_t bytes_written_in_last, + int32_t total_bytes_written, + bool need_comma, + az_json_token_kind token_kind) +{ + ref_json_writer->_internal.bytes_written += bytes_written_in_last; + ref_json_writer->_internal.total_bytes_written += total_bytes_written; + ref_json_writer->_internal.need_comma = need_comma; + ref_json_writer->_internal.token_kind = token_kind; +} + +static AZ_NODISCARD az_result az_json_writer_span_copy_chunked( + az_json_writer* ref_json_writer, + az_span* remaining_json, + az_span value) +{ + if (az_span_size(value) < az_span_size(*remaining_json)) + { + *remaining_json = az_span_copy(*remaining_json, value); + ref_json_writer->_internal.bytes_written += az_span_size(value); + } + else + { + while (az_span_size(value) != 0) + { + int32_t destination_size = az_span_size(*remaining_json); + az_span value_slice_that_fits = value; + if (destination_size < az_span_size(value)) + { + value_slice_that_fits = az_span_slice(value, 0, destination_size); + } + + az_span_copy(*remaining_json, value_slice_that_fits); + ref_json_writer->_internal.bytes_written += az_span_size(value_slice_that_fits); + + value = az_span_slice_to_end(value, az_span_size(value_slice_that_fits)); + *remaining_json = _get_remaining_span(ref_json_writer, _az_MINIMUM_STRING_CHUNK_SIZE); + _az_RETURN_IF_NOT_ENOUGH_SIZE(*remaining_json, _az_MINIMUM_STRING_CHUNK_SIZE); + } + } + return AZ_OK; +} + +static AZ_NODISCARD az_result +az_json_writer_append_string_small(az_json_writer* ref_json_writer, az_span value) +{ + _az_PRECONDITION(az_span_size(value) <= _az_MAX_UNESCAPED_STRING_SIZE_PER_CHUNK); + + int32_t required_size = 2; // For the surrounding quotes. + + if (ref_json_writer->_internal.need_comma) + { + required_size++; // For the leading comma separator. + } + + int32_t index_of_first_escaped_char = -1; + required_size += _az_json_writer_escaped_length(value, &index_of_first_escaped_char, false); + + _az_PRECONDITION(required_size <= _az_MINIMUM_STRING_CHUNK_SIZE); + + az_span remaining_json = _get_remaining_span(ref_json_writer, required_size); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remaining_json, required_size); + + if (ref_json_writer->_internal.need_comma) + { + remaining_json = az_span_copy_u8(remaining_json, ','); + } + + remaining_json = az_span_copy_u8(remaining_json, '"'); + + // No character needed to be escaped, copy the whole string as is. + if (index_of_first_escaped_char == -1) + { + remaining_json = az_span_copy(remaining_json, value); + } + else + { + // Bulk copy the characters that didn't need to be escaped before dropping to the byte-by-byte + // encode and copy. + remaining_json + = az_span_copy(remaining_json, az_span_slice(value, 0, index_of_first_escaped_char)); + remaining_json = _az_json_writer_escape_and_copy( + remaining_json, az_span_slice_to_end(value, index_of_first_escaped_char)); + } + + az_span_copy_u8(remaining_json, '"'); + + _az_update_json_writer_state( + ref_json_writer, required_size, required_size, true, AZ_JSON_TOKEN_STRING); + return AZ_OK; +} + +static AZ_NODISCARD az_result +az_json_writer_append_string_chunked(az_json_writer* ref_json_writer, az_span value) +{ + _az_PRECONDITION(az_span_size(value) > _az_MAX_UNESCAPED_STRING_SIZE_PER_CHUNK); + + az_span remaining_json = _get_remaining_span(ref_json_writer, _az_MINIMUM_STRING_CHUNK_SIZE); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remaining_json, _az_MINIMUM_STRING_CHUNK_SIZE); + + int32_t required_size = 2; // For the surrounding quotes. + if (ref_json_writer->_internal.need_comma) + { + remaining_json = az_span_copy_u8(remaining_json, ','); + required_size++; + ref_json_writer->_internal.bytes_written++; + } + + remaining_json = az_span_copy_u8(remaining_json, '"'); + ref_json_writer->_internal.bytes_written++; + + int32_t consumed = 0; + do + { + az_span value_slice = az_span_slice_to_end(value, consumed); + int32_t index_of_first_escaped_char = -1; + _az_json_writer_escaped_length(value_slice, &index_of_first_escaped_char, true); + + // No character needed to be escaped, copy the whole string as is. + if (index_of_first_escaped_char == -1) + { + _az_RETURN_IF_FAILED( + az_json_writer_span_copy_chunked(ref_json_writer, &remaining_json, value_slice)); + consumed += az_span_size(value_slice); + } + else + { + // Bulk copy the characters that didn't need to be escaped before dropping to the byte-by-byte + // encode and copy. + _az_RETURN_IF_FAILED(az_json_writer_span_copy_chunked( + ref_json_writer, + &remaining_json, + az_span_slice(value_slice, 0, index_of_first_escaped_char))); + + consumed += index_of_first_escaped_char; + + uint8_t* value_ptr = az_span_ptr(value_slice); + uint8_t const ch = value_ptr[index_of_first_escaped_char]; + + remaining_json = _get_remaining_span(ref_json_writer, _az_MINIMUM_STRING_CHUNK_SIZE); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remaining_json, _az_MINIMUM_STRING_CHUNK_SIZE); + + int32_t written = _az_json_writer_escape_next_byte_and_copy(&remaining_json, ch); + ref_json_writer->_internal.bytes_written += written; + + // Only account for the difference in the number of bytes written when escaped + // compared to when the bytes are copied as is. + required_size += written - 1; + + consumed++; + } + } while (consumed < az_span_size(value)); + + remaining_json = _get_remaining_span(ref_json_writer, _az_MINIMUM_STRING_CHUNK_SIZE); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remaining_json, _az_MINIMUM_STRING_CHUNK_SIZE); + + az_span_copy_u8(remaining_json, '"'); + ref_json_writer->_internal.bytes_written++; + + // Currently, required_size only counts the escaped bytes, so add back the length of the input + // string (consumed == az_span_size(value)); + required_size += consumed; + + // We already tracked and updated bytes_written while writing, so no need to update it here. + _az_update_json_writer_state(ref_json_writer, 0, required_size, true, AZ_JSON_TOKEN_STRING); + return AZ_OK; +} + +AZ_NODISCARD az_result az_json_writer_append_string(az_json_writer* ref_json_writer, az_span value) +{ + _az_PRECONDITION_NOT_NULL(ref_json_writer); + // An empty span is allowed, and we write an empty JSON string for it. + _az_PRECONDITION_VALID_SPAN(value, 0, true); + _az_PRECONDITION(az_span_size(value) <= _az_MAX_UNESCAPED_STRING_SIZE); + _az_PRECONDITION(_az_is_appending_value_valid(ref_json_writer)); + + if (az_span_size(value) <= _az_MAX_UNESCAPED_STRING_SIZE_PER_CHUNK) + { + return az_json_writer_append_string_small(ref_json_writer, value); + } + + return az_json_writer_append_string_chunked(ref_json_writer, value); +} + +static AZ_NODISCARD az_result +az_json_writer_append_property_name_small(az_json_writer* ref_json_writer, az_span value) +{ + _az_PRECONDITION(az_span_size(value) <= _az_MAX_UNESCAPED_STRING_SIZE_PER_CHUNK); + + int32_t required_size = 3; // For the surrounding quotes and the key:value separator colon. + + if (ref_json_writer->_internal.need_comma) + { + required_size++; // For the leading comma separator. + } + + int32_t index_of_first_escaped_char = -1; + required_size += _az_json_writer_escaped_length(value, &index_of_first_escaped_char, false); + + _az_PRECONDITION(required_size <= _az_MINIMUM_STRING_CHUNK_SIZE); + + az_span remaining_json = _get_remaining_span(ref_json_writer, required_size); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remaining_json, required_size); + + if (ref_json_writer->_internal.need_comma) + { + remaining_json = az_span_copy_u8(remaining_json, ','); + } + + remaining_json = az_span_copy_u8(remaining_json, '"'); + + // No character needed to be escaped, copy the whole string as is. + if (index_of_first_escaped_char == -1) + { + remaining_json = az_span_copy(remaining_json, value); + } + else + { + // Bulk copy the characters that didn't need to be escaped before dropping to the byte-by-byte + // encode and copy. + remaining_json + = az_span_copy(remaining_json, az_span_slice(value, 0, index_of_first_escaped_char)); + remaining_json = _az_json_writer_escape_and_copy( + remaining_json, az_span_slice_to_end(value, index_of_first_escaped_char)); + } + + remaining_json = az_span_copy_u8(remaining_json, '"'); + az_span_copy_u8(remaining_json, ':'); + + _az_update_json_writer_state( + ref_json_writer, required_size, required_size, false, AZ_JSON_TOKEN_PROPERTY_NAME); + return AZ_OK; +} + +static AZ_NODISCARD az_result +az_json_writer_append_property_name_chunked(az_json_writer* ref_json_writer, az_span value) +{ + _az_PRECONDITION(az_span_size(value) > _az_MAX_UNESCAPED_STRING_SIZE_PER_CHUNK); + + az_span remaining_json = _get_remaining_span(ref_json_writer, _az_MINIMUM_STRING_CHUNK_SIZE); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remaining_json, _az_MINIMUM_STRING_CHUNK_SIZE); + + int32_t required_size = 3; // For the surrounding quotes and the key:value separator colon. + if (ref_json_writer->_internal.need_comma) + { + remaining_json = az_span_copy_u8(remaining_json, ','); + required_size++; + ref_json_writer->_internal.bytes_written++; + } + + remaining_json = az_span_copy_u8(remaining_json, '"'); + ref_json_writer->_internal.bytes_written++; + + int32_t consumed = 0; + do + { + az_span value_slice = az_span_slice_to_end(value, consumed); + int32_t index_of_first_escaped_char = -1; + _az_json_writer_escaped_length(value_slice, &index_of_first_escaped_char, true); + + // No character needed to be escaped, copy the whole string as is. + if (index_of_first_escaped_char == -1) + { + _az_RETURN_IF_FAILED( + az_json_writer_span_copy_chunked(ref_json_writer, &remaining_json, value_slice)); + consumed += az_span_size(value_slice); + } + else + { + // Bulk copy the characters that didn't need to be escaped before dropping to the byte-by-byte + // encode and copy. + _az_RETURN_IF_FAILED(az_json_writer_span_copy_chunked( + ref_json_writer, + &remaining_json, + az_span_slice(value_slice, 0, index_of_first_escaped_char))); + + consumed += index_of_first_escaped_char; + + uint8_t* value_ptr = az_span_ptr(value_slice); + uint8_t const ch = value_ptr[index_of_first_escaped_char]; + + remaining_json = _get_remaining_span(ref_json_writer, _az_MINIMUM_STRING_CHUNK_SIZE); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remaining_json, _az_MINIMUM_STRING_CHUNK_SIZE); + + int32_t written = _az_json_writer_escape_next_byte_and_copy(&remaining_json, ch); + ref_json_writer->_internal.bytes_written += written; + + // Only account for the difference in the number of bytes written when escaped + // compared to when the bytes are copied as is. + required_size += written - 1; + + consumed++; + } + } while (consumed < az_span_size(value)); + + remaining_json = _get_remaining_span(ref_json_writer, _az_MINIMUM_STRING_CHUNK_SIZE); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remaining_json, _az_MINIMUM_STRING_CHUNK_SIZE); + + remaining_json = az_span_copy_u8(remaining_json, '"'); + remaining_json = az_span_copy_u8(remaining_json, ':'); + ref_json_writer->_internal.bytes_written += 2; + + // Currently, required_size only counts the escaped bytes, so add back the length of the input + // string (consumed == az_span_size(value)); + required_size += consumed; + + // We already tracked and updated bytes_written while writing, so no need to update it here. + _az_update_json_writer_state( + ref_json_writer, 0, required_size, false, AZ_JSON_TOKEN_PROPERTY_NAME); + return AZ_OK; +} + +AZ_NODISCARD az_result +az_json_writer_append_property_name(az_json_writer* ref_json_writer, az_span name) +{ + // TODO: Consider refactoring to reduce duplication between writing property name and string. + _az_PRECONDITION_NOT_NULL(ref_json_writer); + _az_PRECONDITION_VALID_SPAN(name, 0, false); + _az_PRECONDITION(az_span_size(name) <= _az_MAX_UNESCAPED_STRING_SIZE); + _az_PRECONDITION(_az_is_appending_property_name_valid(ref_json_writer)); + + if (az_span_size(name) <= _az_MAX_UNESCAPED_STRING_SIZE_PER_CHUNK) + { + return az_json_writer_append_property_name_small(ref_json_writer, name); + } + + return az_json_writer_append_property_name_chunked(ref_json_writer, name); +} + +static AZ_NODISCARD az_result _az_validate_json( + az_span json_text, + az_json_token_kind* first_token_kind, + az_json_token_kind* last_token_kind) +{ + _az_PRECONDITION_NOT_NULL(first_token_kind); + + az_json_reader reader = { 0 }; + _az_RETURN_IF_FAILED(az_json_reader_init(&reader, json_text, NULL)); + + az_result result = az_json_reader_next_token(&reader); + _az_RETURN_IF_FAILED(result); + + // This is guaranteed not to be a property name or end object/array. + // The first token of a valid JSON must either be a value or start object/array. + *first_token_kind = reader.token.kind; + + // Keep reading until we have finished validating the entire JSON text and make sure it isn't + // incomplete. + while (az_result_succeeded(result = az_json_reader_next_token(&reader))) + { + } + + if (result != AZ_ERROR_JSON_READER_DONE) + { + return result; + } + + // This is guaranteed not to be a property name or start object/array. + // The last token of a valid JSON must either be a value or end object/array. + *last_token_kind = reader.token.kind; + + return AZ_OK; +} + +AZ_NODISCARD az_result +az_json_writer_append_json_text(az_json_writer* ref_json_writer, az_span json_text) +{ + _az_PRECONDITION_NOT_NULL(ref_json_writer); + // A null or empty span is not allowed since that is invalid JSON. + _az_PRECONDITION_VALID_SPAN(json_text, 0, false); + + az_json_token_kind first_token_kind = AZ_JSON_TOKEN_NONE; + az_json_token_kind last_token_kind = AZ_JSON_TOKEN_NONE; + + // This runtime validation is necessary since the input could be user defined and malformed. + // This cannot be caught at dev time by a precondition, especially since they can be turned off. + _az_RETURN_IF_FAILED(_az_validate_json(json_text, &first_token_kind, &last_token_kind)); + + // It is guaranteed that first_token_kind is NOT: + // AZ_JSON_TOKEN_NONE, AZ_JSON_TOKEN_END_ARRAY, AZ_JSON_TOKEN_END_OBJECT, + // AZ_JSON_TOKEN_PROPERTY_NAME + // And that last_token_kind is NOT: + // AZ_JSON_TOKEN_NONE, AZ_JSON_TOKEN_START_ARRAY, AZ_JSON_TOKEN_START_OBJECT, + // AZ_JSON_TOKEN_PROPERTY_NAME + + // The JSON text is valid, but appending it to the the JSON writer at the current state still may + // not be valid. + if (!_az_is_appending_value_valid(ref_json_writer)) + { + // All other tokens, including start array and object are validated here. + // Also first_token_kind cannot be AZ_JSON_TOKEN_NONE at this point. + return AZ_ERROR_JSON_INVALID_STATE; + } + + az_span remaining_json = _get_remaining_span(ref_json_writer, _az_MINIMUM_STRING_CHUNK_SIZE); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remaining_json, _az_MINIMUM_STRING_CHUNK_SIZE); + + int32_t required_size = az_span_size(json_text); + if (ref_json_writer->_internal.need_comma) + { + remaining_json = az_span_copy_u8(remaining_json, ','); + ref_json_writer->_internal.bytes_written++; + required_size++; // For the leading comma separator. + } + + _az_RETURN_IF_FAILED( + az_json_writer_span_copy_chunked(ref_json_writer, &remaining_json, json_text)); + + // We only need to add a comma if the last token we append is a value or end of object/array. + // If the last token is a property name or the start of an object/array, we don't need to add a + // comma before appending subsequent tokens. + // However, there is no valid, complete, single JSON value where the last token would be property + // name, or start object/array. + // Therefore, need_comma must be true after appending the json_text. + + // We already tracked and updated bytes_written while writing, so no need to update it here. + _az_update_json_writer_state(ref_json_writer, 0, required_size, true, last_token_kind); + return AZ_OK; +} + +static AZ_NODISCARD az_result _az_json_writer_append_literal( + az_json_writer* ref_json_writer, + az_span literal, + az_json_token_kind literal_kind) +{ + _az_PRECONDITION_NOT_NULL(ref_json_writer); + _az_PRECONDITION( + literal_kind == AZ_JSON_TOKEN_NULL || literal_kind == AZ_JSON_TOKEN_TRUE + || literal_kind == AZ_JSON_TOKEN_FALSE); + _az_PRECONDITION_VALID_SPAN(literal, 4, false); + _az_PRECONDITION(az_span_size(literal) <= 5); // null, true, or false + _az_PRECONDITION(_az_is_appending_value_valid(ref_json_writer)); + + int32_t required_size = az_span_size(literal); + + if (ref_json_writer->_internal.need_comma) + { + required_size++; // For the leading comma separator. + } + + az_span remaining_json = _get_remaining_span(ref_json_writer, required_size); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remaining_json, required_size); + + if (ref_json_writer->_internal.need_comma) + { + remaining_json = az_span_copy_u8(remaining_json, ','); + } + + az_span_copy(remaining_json, literal); + + _az_update_json_writer_state(ref_json_writer, required_size, required_size, true, literal_kind); + return AZ_OK; +} + +AZ_NODISCARD az_result az_json_writer_append_bool(az_json_writer* ref_json_writer, bool value) +{ + return value ? _az_json_writer_append_literal( + ref_json_writer, AZ_SPAN_FROM_STR("true"), AZ_JSON_TOKEN_TRUE) + : _az_json_writer_append_literal( + ref_json_writer, AZ_SPAN_FROM_STR("false"), AZ_JSON_TOKEN_FALSE); +} + +AZ_NODISCARD az_result az_json_writer_append_null(az_json_writer* ref_json_writer) +{ + return _az_json_writer_append_literal( + ref_json_writer, AZ_SPAN_FROM_STR("null"), AZ_JSON_TOKEN_NULL); +} + +AZ_NODISCARD az_result az_json_writer_append_int32(az_json_writer* ref_json_writer, int32_t value) +{ + _az_PRECONDITION_NOT_NULL(ref_json_writer); + _az_PRECONDITION(_az_is_appending_value_valid(ref_json_writer)); + + int32_t required_size = _az_MAX_SIZE_FOR_INT32; // Need enough space to write any 32-bit integer. + + if (ref_json_writer->_internal.need_comma) + { + required_size++; // For the leading comma separator. + } + + az_span remaining_json = _get_remaining_span(ref_json_writer, required_size); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remaining_json, required_size); + + if (ref_json_writer->_internal.need_comma) + { + remaining_json = az_span_copy_u8(remaining_json, ','); + } + + // Since we asked for the maximum needed space above, this is guaranteed not to fail due to + // AZ_ERROR_NOT_ENOUGH_SPACE. Still checking the returned az_result, for other potential failure + // cases. + az_span leftover; + _az_RETURN_IF_FAILED(az_span_i32toa(remaining_json, value, &leftover)); + + // We already accounted for the maximum size needed in required_size, so subtract that to get the + // actual bytes written. + int32_t written + = required_size + _az_span_diff(leftover, remaining_json) - _az_MAX_SIZE_FOR_INT32; + _az_update_json_writer_state(ref_json_writer, written, written, true, AZ_JSON_TOKEN_NUMBER); + return AZ_OK; +} + +AZ_NODISCARD az_result az_json_writer_append_double( + az_json_writer* ref_json_writer, + double value, + int32_t fractional_digits) +{ + _az_PRECONDITION_NOT_NULL(ref_json_writer); + _az_PRECONDITION(_az_is_appending_value_valid(ref_json_writer)); + // Non-finite numbers are not supported because they lead to invalid JSON. + // Unquoted strings such as nan and -inf are invalid as JSON numbers. + _az_PRECONDITION(_az_isfinite(value)); + _az_PRECONDITION_RANGE(0, fractional_digits, _az_MAX_SUPPORTED_FRACTIONAL_DIGITS); + + // Need enough space to write any double number. + int32_t required_size = _az_MAX_SIZE_FOR_WRITING_DOUBLE; + + if (ref_json_writer->_internal.need_comma) + { + required_size++; // For the leading comma separator. + } + + az_span remaining_json = _get_remaining_span(ref_json_writer, required_size); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remaining_json, required_size); + + if (ref_json_writer->_internal.need_comma) + { + remaining_json = az_span_copy_u8(remaining_json, ','); + } + + // Since we asked for the maximum needed space above, this is guaranteed not to fail due to + // AZ_ERROR_NOT_ENOUGH_SPACE. Still checking the returned az_result, for other potential failure + // cases. + az_span leftover; + _az_RETURN_IF_FAILED(az_span_dtoa(remaining_json, value, fractional_digits, &leftover)); + + // We already accounted for the maximum size needed in required_size, so subtract that to get the + // actual bytes written. + int32_t written + = required_size + _az_span_diff(leftover, remaining_json) - _az_MAX_SIZE_FOR_WRITING_DOUBLE; + _az_update_json_writer_state(ref_json_writer, written, written, true, AZ_JSON_TOKEN_NUMBER); + return AZ_OK; +} + +static AZ_NODISCARD az_result _az_json_writer_append_container_start( + az_json_writer* ref_json_writer, + uint8_t byte, + az_json_token_kind container_kind) +{ + _az_PRECONDITION_NOT_NULL(ref_json_writer); + _az_PRECONDITION( + container_kind == AZ_JSON_TOKEN_BEGIN_OBJECT || container_kind == AZ_JSON_TOKEN_BEGIN_ARRAY); + _az_PRECONDITION(_az_is_appending_value_valid(ref_json_writer)); + + // The current depth is equal to or larger than the maximum allowed depth of 64. Cannot write the + // next JSON object or array. + if (ref_json_writer->_internal.bit_stack._internal.current_depth >= _az_MAX_JSON_STACK_SIZE) + { + return AZ_ERROR_JSON_NESTING_OVERFLOW; + } + + int32_t required_size = 1; // For the start object or array byte. + + if (ref_json_writer->_internal.need_comma) + { + required_size++; // For the leading comma separator. + } + + az_span remaining_json = _get_remaining_span(ref_json_writer, required_size); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remaining_json, required_size); + + if (ref_json_writer->_internal.need_comma) + { + remaining_json = az_span_copy_u8(remaining_json, ','); + } + + az_span_copy_u8(remaining_json, byte); + + _az_update_json_writer_state( + ref_json_writer, required_size, required_size, false, container_kind); + if (container_kind == AZ_JSON_TOKEN_BEGIN_OBJECT) + { + _az_json_stack_push(&ref_json_writer->_internal.bit_stack, _az_JSON_STACK_OBJECT); + } + else + { + _az_json_stack_push(&ref_json_writer->_internal.bit_stack, _az_JSON_STACK_ARRAY); + } + + return AZ_OK; +} + +AZ_NODISCARD az_result az_json_writer_append_begin_object(az_json_writer* ref_json_writer) +{ + return _az_json_writer_append_container_start(ref_json_writer, '{', AZ_JSON_TOKEN_BEGIN_OBJECT); +} + +AZ_NODISCARD az_result az_json_writer_append_begin_array(az_json_writer* ref_json_writer) +{ + return _az_json_writer_append_container_start(ref_json_writer, '[', AZ_JSON_TOKEN_BEGIN_ARRAY); +} + +static AZ_NODISCARD az_result az_json_writer_append_container_end( + az_json_writer* ref_json_writer, + uint8_t byte, + az_json_token_kind container_kind) +{ + _az_PRECONDITION_NOT_NULL(ref_json_writer); + _az_PRECONDITION( + container_kind == AZ_JSON_TOKEN_END_OBJECT || container_kind == AZ_JSON_TOKEN_END_ARRAY); + _az_PRECONDITION(_az_is_appending_container_end_valid(ref_json_writer, byte)); + + int32_t required_size = 1; // For the end object or array byte. + + az_span remaining_json = _get_remaining_span(ref_json_writer, required_size); + _az_RETURN_IF_NOT_ENOUGH_SIZE(remaining_json, required_size); + + az_span_copy_u8(remaining_json, byte); + + _az_update_json_writer_state(ref_json_writer, required_size, required_size, true, container_kind); + _az_json_stack_pop(&ref_json_writer->_internal.bit_stack); + + return AZ_OK; +} + +AZ_NODISCARD az_result az_json_writer_append_end_object(az_json_writer* ref_json_writer) +{ + return az_json_writer_append_container_end(ref_json_writer, '}', AZ_JSON_TOKEN_END_OBJECT); +} + +AZ_NODISCARD az_result az_json_writer_append_end_array(az_json_writer* ref_json_writer) +{ + return az_json_writer_append_container_end(ref_json_writer, ']', AZ_JSON_TOKEN_END_ARRAY); +} diff --git a/src/az_log.c b/src/az_log.c new file mode 100644 index 00000000..d2047112 --- /dev/null +++ b/src/az_log.c @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "az_span_private.h" +#include +#include +#include +#include +#include +#include +#include + +#include + +#include <_az_cfg.h> + +#ifndef AZ_NO_LOGGING + +// Only using volatile here, not for thread safety, but so that the compiler does not optimize what +// it falsely thinks are stale reads. +static az_log_message_fn volatile _az_log_message_callback = NULL; +static az_log_classification_filter_fn volatile _az_message_filter_callback = NULL; + +void az_log_set_message_callback(az_log_message_fn log_message_callback) +{ + // We assume assignments are atomic for the supported platforms and compilers. + _az_log_message_callback = log_message_callback; +} + +void az_log_set_classification_filter_callback( + az_log_classification_filter_fn message_filter_callback) +{ + // We assume assignments are atomic for the supported platforms and compilers. + _az_message_filter_callback = message_filter_callback; +} + +AZ_INLINE az_log_message_fn _az_log_get_message_callback(az_log_classification classification) +{ + _az_PRECONDITION(classification > 0); + + // Copy the volatile fields to local variables so that they don't change within this function. + az_log_message_fn const message_callback = _az_log_message_callback; + az_log_classification_filter_fn const message_filter_callback = _az_message_filter_callback; + + // If the user hasn't registered a message_filter_callback, then we log everything, as long as a + // message_callback method was provided. + // Otherwise, we log only what that filter allows. + if (message_callback != NULL + && (message_filter_callback == NULL || message_filter_callback(classification))) + { + return message_callback; + } + + // This message's classification is either not allowed by the filter, or there is no callback + // function registered to receive the message. In both cases, we should not log it. + return NULL; +} + +// This function returns whether or not the passed-in message should be logged. +bool _az_log_should_write(az_log_classification classification) +{ + return _az_log_get_message_callback(classification) != NULL; +} + +// This function attempts to log the passed-in message. +void _az_log_write(az_log_classification classification, az_span message) +{ + _az_PRECONDITION_VALID_SPAN(message, 0, true); + + az_log_message_fn const message_callback = _az_log_get_message_callback(classification); + + if (message_callback != NULL) + { + message_callback(classification, message); + } +} + +#endif // AZ_NO_LOGGING diff --git a/src/az_log.h b/src/az_log.h new file mode 100644 index 00000000..c097244f --- /dev/null +++ b/src/az_log.h @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief This header defines the types and functions your application uses to be notified of Azure + * SDK client library log messages. + * + * @details If you define the `AZ_NO_LOGGING` symbol when compiling the SDK code (or adding option + * `-DLOGGING=OFF` with cmake), all of the Azure SDK logging functionality will be excluded, making + * the resulting compiled code smaller and faster. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_LOG_H +#define _az_LOG_H + +#include +#include + +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief Identifies the classifications of log messages produced by the SDK. + * + * @note See the following `az_log_classification` values from various headers: + * - #az_log_classification_core + * - #az_log_classification_iot + */ +typedef int32_t az_log_classification; + +// az_log_classification Bits: +// - 31 Always 0. +// - 16..30 Facility. +// - 0..15 Code. + +#define _az_LOG_MAKE_CLASSIFICATION(facility, code) \ + ((az_log_classification)(((uint32_t)(facility) << 16U) | (uint32_t)(code))) + +/** + * @brief Identifies the #az_log_classification produced by the SDK Core. + */ +enum az_log_classification_core +{ + AZ_LOG_HTTP_REQUEST + = _az_LOG_MAKE_CLASSIFICATION(_az_FACILITY_CORE_HTTP, 1), ///< HTTP request is about to be sent. + + AZ_LOG_HTTP_RESPONSE + = _az_LOG_MAKE_CLASSIFICATION(_az_FACILITY_CORE_HTTP, 2), ///< HTTP response was received. + + AZ_LOG_HTTP_RETRY = _az_LOG_MAKE_CLASSIFICATION( + _az_FACILITY_CORE_HTTP, + 3), ///< First HTTP request did not succeed and will be retried. +}; + +/** + * @brief Defines the signature of the callback function that application developers must provide to + * receive Azure SDK log messages. + * + * @param[in] classification The log message's #az_log_classification. + * @param[in] message The log message. + */ +typedef void (*az_log_message_fn)(az_log_classification classification, az_span message); + +/** + * @brief Defines the signature of the callback function that application developers must provide + * which will be used to check whether a particular log classification should be logged. + * + * @param[in] classification The log message's #az_log_classification. + * + * @return Whether or not a log message with the provided classification should be logged. + */ +typedef bool (*az_log_classification_filter_fn)(az_log_classification classification); + +/** + * @brief Sets the functions that will be invoked to report an SDK log message. + * + * @param[in] log_message_callback __[nullable]__ A pointer to the function that will be invoked + * when the SDK reports a log message that should be logged according to the result of the + * #az_log_classification_filter_fn provided to #az_log_set_classification_filter_callback(). If + * `NULL`, no function will be invoked. + * + * @remarks By default, this is `NULL`, which means, no function is invoked. + */ +#ifndef AZ_NO_LOGGING +void az_log_set_message_callback(az_log_message_fn log_message_callback); +#else +AZ_INLINE void az_log_set_message_callback(az_log_message_fn log_message_callback) +{ + (void)log_message_callback; +} +#endif // AZ_NO_LOGGING + +/** + * @brief Sets the functions that will be invoked to check whether an SDK log message should be + * reported. + * + * @param[in] message_filter_callback __[nullable]__ A pointer to the function that will be invoked + * when the SDK checks whether a log message of a particular #az_log_classification should be + * logged. If `NULL`, log messages for all classifications will be logged, by passing them to the + * #az_log_message_fn provided to #az_log_set_message_callback(). + * + * @remarks By default, this is `NULL`, in which case no function is invoked to check whether a + * classification should be logged or not. The SDK assumes true, passing messages with any log + * classification to the #az_log_message_fn provided to #az_log_set_message_callback(). + */ +#ifndef AZ_NO_LOGGING +void az_log_set_classification_filter_callback( + az_log_classification_filter_fn message_filter_callback); +#else +AZ_INLINE void az_log_set_classification_filter_callback( + az_log_classification_filter_fn message_filter_callback) +{ + (void)message_filter_callback; +} +#endif // AZ_NO_LOGGING + +#include <_az_cfg_suffix.h> + +#endif // _az_LOG_H diff --git a/src/az_log_internal.h b/src/az_log_internal.h new file mode 100644 index 00000000..55adf0ca --- /dev/null +++ b/src/az_log_internal.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#ifndef _az_LOG_INTERNAL_H +#define _az_LOG_INTERNAL_H + +#include +#include + +#include + +#include <_az_cfg_prefix.h> + +#ifndef AZ_NO_LOGGING + +bool _az_log_should_write(az_log_classification classification); +void _az_log_write(az_log_classification classification, az_span message); + +#define _az_LOG_SHOULD_WRITE(classification) _az_log_should_write(classification) +#define _az_LOG_WRITE(classification, message) _az_log_write(classification, message) + +#else + +#define _az_LOG_SHOULD_WRITE(classification) false + +#define _az_LOG_WRITE(classification, message) + +#endif // AZ_NO_LOGGING + +#include <_az_cfg_suffix.h> + +#endif // _az_LOG_INTERNAL_H diff --git a/src/az_nohttp.c b/src/az_nohttp.c new file mode 100644 index 00000000..27d21eb0 --- /dev/null +++ b/src/az_nohttp.c @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include + +#include <_az_cfg.h> + +AZ_NODISCARD az_result +az_http_client_send_request(az_http_request const* request, az_http_response* ref_response) +{ + (void)request; + (void)ref_response; + return AZ_ERROR_DEPENDENCY_NOT_PROVIDED; +} diff --git a/src/az_noplatform.c b/src/az_noplatform.c new file mode 100644 index 00000000..02ee8872 --- /dev/null +++ b/src/az_noplatform.c @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include +#include + +#include <_az_cfg.h> + +AZ_NODISCARD az_result az_platform_clock_msec(int64_t* out_clock_msec) +{ + _az_PRECONDITION_NOT_NULL(out_clock_msec); + *out_clock_msec = 0; + return AZ_ERROR_DEPENDENCY_NOT_PROVIDED; +} + +AZ_NODISCARD az_result az_platform_sleep_msec(int32_t milliseconds) +{ + (void)milliseconds; + return AZ_ERROR_DEPENDENCY_NOT_PROVIDED; +} diff --git a/src/az_platform.h b/src/az_platform.h new file mode 100644 index 00000000..1fb1f90c --- /dev/null +++ b/src/az_platform.h @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Defines platform-specific functionality used by the Azure SDK. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_PLATFORM_H +#define _az_PLATFORM_H + +#include + +#include +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief Gets the platform clock in milliseconds. + * + * @remark The moment of time where clock starts is undefined, but if this function is getting + * called twice with one second interval, the difference between the values returned should be equal + * to 1000. + * + * @param[out] out_clock_msec Platform clock in milliseconds. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_DEPENDENCY_NOT_PROVIDED No platform implementation was supplied to support this + * function. + */ +AZ_NODISCARD az_result az_platform_clock_msec(int64_t* out_clock_msec); + +/** + * @brief Tells the platform to sleep for a given number of milliseconds. + * + * @param[in] milliseconds Number of milliseconds to sleep. + * + * @remarks The behavior is undefined when \p milliseconds is a non-positive value (0 or less than + * 0). + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_DEPENDENCY_NOT_PROVIDED No platform implementation was supplied to support this + * function. + */ +AZ_NODISCARD az_result az_platform_sleep_msec(int32_t milliseconds); + +#include <_az_cfg_suffix.h> + +#endif // _az_PLATFORM_H diff --git a/src/az_precondition.c b/src/az_precondition.c new file mode 100644 index 00000000..baf94e6b --- /dev/null +++ b/src/az_precondition.c @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include +#include + +#include <_az_cfg.h> + +static void az_precondition_failed_default() +{ + /* By default, when a precondition fails the calling thread spins forever */ + while (1) + { + } +} + +az_precondition_failed_fn _az_precondition_failed_callback = az_precondition_failed_default; + +void az_precondition_failed_set_callback(az_precondition_failed_fn az_precondition_failed_callback) +{ + _az_precondition_failed_callback = az_precondition_failed_callback; +} + +az_precondition_failed_fn az_precondition_failed_get_callback() +{ + return _az_precondition_failed_callback; +} diff --git a/src/az_precondition.h b/src/az_precondition.h new file mode 100644 index 00000000..abc2e9f5 --- /dev/null +++ b/src/az_precondition.h @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief This header defines the types and functions your application uses to override the default + * precondition failure behavior. + * + * Public SDK functions validate the arguments passed to them in an effort to ensure that calling + * code is passing valid values. The valid value is called a contract precondition. If an SDK + * function detects a precondition failure (invalid argument value), then by default, it calls a + * function that places the calling thread into an infinite sleep state; other threads continue to + * run. + * + * To override the default behavior, implement a function matching the #az_precondition_failed_fn + * function signature and then, in your application's initialization (before calling any Azure SDK + * function), call #az_precondition_failed_set_callback() passing it the address of your function. + * Now, when any Azure SDK function detects a precondition failure, it will invoke your callback + * instead. You might override the callback to attach a debugger or perhaps to reboot the device + * rather than allowing it to continue running with unpredictable behavior. + * + * Also, if you define the `AZ_NO_PRECONDITION_CHECKING` symbol when compiling the SDK code (or + * adding option `-DPRECONDITIONS=OFF` with cmake), all of the Azure SDK precondition checking will + * be excluded, making the resulting compiled code smaller and faster. We recommend doing this + * before you ship your code. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_PRECONDITION_H +#define _az_PRECONDITION_H + +#include +#include + +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief Defines the signature of the callback function that application developers can write in + * order to override the default precondition failure behavior. + */ +typedef void (*az_precondition_failed_fn)(); + +/** + * @brief Allows your application to override the default behavior in response to an SDK function + * detecting an invalid argument (precondition failure). + * + * Call this function once when your application initializes and before you call and Azure SDK + * functions. + * + * @param[in] az_precondition_failed_callback A pointer to the function that will be invoked when an + * Azure SDK function detects a precondition failure. + */ +void az_precondition_failed_set_callback(az_precondition_failed_fn az_precondition_failed_callback); + +#include <_az_cfg_suffix.h> + +#endif // _az_PRECONDITION_H diff --git a/src/az_precondition_internal.h b/src/az_precondition_internal.h new file mode 100644 index 00000000..81b2d59b --- /dev/null +++ b/src/az_precondition_internal.h @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file az_precondition_internal.h + * + * @brief This header defines the types and functions your application uses + * to override the default precondition failure behavior. + * + * Public SDK functions validate the arguments passed to them in an effort + * to ensure that calling code is passing valid values. The valid value is + * called a contract precondition. If an SDK function detects a precondition + * failure (invalid argument value), then by default, it calls a function that + * places the calling thread into an infinite sleep state; other threads + * continue to run. + * + * To override the default behavior, implement a function matching the + * az_precondition_failed_fn function signature and then, in your application's + * initialization (before calling any Azure SDK function), call + * az_precondition_failed_set_callback passing it the address of your function. + * Now, when any Azure SDK function detects a precondition failure, it will invoke + * your callback instead. You might override the callback to attach a debugger or + * perhaps to reboot the device rather than allowing it to continue running with + * unpredictable behavior. + * + * Also, if you define the AZ_NO_PRECONDITION_CHECKING symbol when compiling the SDK + * code (or adding option -DPRECONDITIONS=OFF with cmake), all of the Azure SDK + * precondition checking will be excluding making the binary code smaller and faster. We + * recommend doing this before you ship your code. + */ + +#ifndef _az_PRECONDITION_INTERNAL_H +#define _az_PRECONDITION_INTERNAL_H + +#include + +#include + +#include +#include + +#include <_az_cfg_prefix.h> + +az_precondition_failed_fn az_precondition_failed_get_callback(); + +// __analysis_assume() tells MSVC's code analysis tool about the assumptions we have, so it doesn't +// emit warnings for the statements that we put into _az_PRECONDITION(). +// Code analysis starts to fail with this condition some time around version 19.27; but +// __analysis_assume() does appear at least as early as version 1920 (The first "Visual Studio +// 2019"). So, we do __analysis_assume() for MSVCs starting with 2019. We don't want to go much +// earlier without verifying, because if __analysis_assume() is not available on earlier compiler +// version, there will be a compilation error. +// For more info, see +// https://docs.microsoft.com/windows-hardware/drivers/devtest/using-the--analysis-assume-function-to-suppress-false-defects +#if _MSC_VER >= 1920 +#define _az_ANALYSIS_ASSUME(statement) __analysis_assume(statement) +#else +#define _az_ANALYSIS_ASSUME(statement) +#endif + +#ifdef AZ_NO_PRECONDITION_CHECKING +#define _az_PRECONDITION(condition) +#else +#define _az_PRECONDITION(condition) \ + do \ + { \ + if (!(condition)) \ + { \ + az_precondition_failed_get_callback()(); \ + } \ + _az_ANALYSIS_ASSUME(condition); \ + } while (0) +#endif // AZ_NO_PRECONDITION_CHECKING + +#define _az_PRECONDITION_RANGE(low, arg, max) _az_PRECONDITION((low) <= (arg) && (arg) <= (max)) + +#define _az_PRECONDITION_NOT_NULL(arg) _az_PRECONDITION((arg) != NULL) +#define _az_PRECONDITION_IS_NULL(arg) _az_PRECONDITION((arg) == NULL) + +AZ_NODISCARD AZ_INLINE bool _az_span_is_valid(az_span span, int32_t min_size, bool null_is_valid) +{ + if (min_size < 0) + { + return false; + } + + uint8_t* const ptr = az_span_ptr(span); + int32_t const span_size = az_span_size(span); + + bool result = false; + + /* Span is valid if: + The size is greater than or equal to a user defined minimum value AND one of the + following: + - If null_is_valid is true and the pointer in the span is null, the size must also be 0. + - In the case of the pointer not being NULL, the size is greater than or equal to zero. + */ + + // On some platforms, in some compilation configurations (Debug), NULL is not 0x0...0. But if you + // initialize a span with { 0 } (or if that span is a part of a structure that is initialized with + // { 0 }) the ptr is not going to be equal to NULL, however the intent of the precondition is to + // disallow default-initialized and null ptrs, so we should treat them the same. + uint8_t* const default_init_ptr = az_span_ptr((az_span){ 0 }); + if (null_is_valid) + { + result = (ptr == NULL || ptr == default_init_ptr) ? span_size == 0 : span_size >= 0; + } + else + { + result = (ptr != NULL && ptr != default_init_ptr) && span_size >= 0; + } + + // Can't wrap over the end of the address space. + // The biggest theoretical pointer value is "(void*)~0" (0xFFFF...), which is the end of address + // space. We don't attempt to read/write beyond the end of the address space - it is unlikely a + // desired behavior, and it is not defined. So, if the span size is greater than the addresses + // left until the theoretical end of the address space, it is not a valid span. + // Example: (az_span) { .ptr = (uint8_t*)(~0 - 5), .size = 10 } is not a valid span, because most + // likely you end up pointing to 0x0000 at .ptr[6], &.ptr[7] is 0x...0001, etc. + uint8_t* const max_ptr = (uint8_t*)~(uint8_t)0; + result = result && ((size_t)span_size <= (size_t)(max_ptr - ptr)); + + return result && min_size <= span_size; +} + +#define _az_PRECONDITION_VALID_SPAN(span, min_size, null_is_valid) \ + _az_PRECONDITION(_az_span_is_valid(span, min_size, null_is_valid)) + +AZ_NODISCARD AZ_INLINE bool _az_span_overlap(az_span a, az_span b) +{ + uint8_t* const a_ptr = az_span_ptr(a); + uint8_t* const b_ptr = az_span_ptr(b); + + return a_ptr <= b_ptr ? (a_ptr + az_span_size(a) > b_ptr) : (b_ptr + az_span_size(b) > a_ptr); +} + +#define _az_PRECONDITION_NO_OVERLAP_SPANS(a, b) _az_PRECONDITION(!_az_span_overlap(a, b)) + +#include <_az_cfg_suffix.h> + +#endif // _az_PRECONDITION_INTERNAL_H diff --git a/src/az_result.h b/src/az_result.h new file mode 100644 index 00000000..5bcd2105 --- /dev/null +++ b/src/az_result.h @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Definition of #az_result and helper functions. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_RESULT_H +#define _az_RESULT_H + +#include +#include + +#include <_az_cfg_prefix.h> + +enum +{ + _az_FACILITY_CORE = 0x1, + _az_FACILITY_CORE_PLATFORM = 0x2, + _az_FACILITY_CORE_JSON = 0x3, + _az_FACILITY_CORE_HTTP = 0x4, + _az_FACILITY_IOT = 0x5, + _az_FACILITY_IOT_MQTT = 0x6, + _az_FACILITY_ULIB = 0x7, +}; + +enum +{ + _az_ERROR_FLAG = (int32_t)0x80000000, +}; + +/** + * @brief The type represents the various success and error conditions. + * + * @note See the following `az_result` values from various headers: + * - #az_result_core + * - #az_result_iot + */ +typedef int32_t az_result; + +// az_result Bits: +// - 31 Severity (0 - success, 1 - failure). +// - 16..30 Facility. +// - 0..15 Code. + +#define _az_RESULT_MAKE_ERROR(facility, code) \ + ((az_result)((uint32_t)_az_ERROR_FLAG | ((uint32_t)(facility) << 16U) | (uint32_t)(code))) + +#define _az_RESULT_MAKE_SUCCESS(facility, code) \ + ((az_result)(((uint32_t)(facility) << 16U) | (uint32_t)(code))) + +/** + * @brief The type represents the various #az_result success and error conditions specific to SDK + * Core. + */ +enum az_result_core +{ + // === Core: Success results ==== + /// Success. + AZ_OK = _az_RESULT_MAKE_SUCCESS(_az_FACILITY_CORE, 0), + + // === Core: Error results === + /// A context was canceled, and a function had to return before result was ready. + AZ_ERROR_CANCELED = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE, 0), + + /// Input argument does not comply with the expected range of values. + AZ_ERROR_ARG = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE, 1), + + /// The destination size is too small for the operation. + AZ_ERROR_NOT_ENOUGH_SPACE = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE, 2), + + /// Requested functionality is not implemented. + AZ_ERROR_NOT_IMPLEMENTED = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE, 3), + + /// Requested item was not found. + AZ_ERROR_ITEM_NOT_FOUND = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE, 4), + + /// Input can't be successfully parsed. + AZ_ERROR_UNEXPECTED_CHAR = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE, 5), + + /// Unexpected end of the input data. + AZ_ERROR_UNEXPECTED_END = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE, 6), + + /// Not supported. + AZ_ERROR_NOT_SUPPORTED = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE, 7), + + /// An external dependency required to perform the operation was not provided. The operation needs + /// an implementation of the platform layer or an HTTP transport adapter. + AZ_ERROR_DEPENDENCY_NOT_PROVIDED = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE, 8), + + // === Platform === + /// Dynamic memory allocation request was not successful. + AZ_ERROR_OUT_OF_MEMORY = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE_PLATFORM, 1), + + // === JSON error codes === + /// The kind of the token being read is not compatible with the expected type of the value. + AZ_ERROR_JSON_INVALID_STATE = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE_JSON, 1), + + /// The JSON depth is too large. + AZ_ERROR_JSON_NESTING_OVERFLOW = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE_JSON, 2), + + /// No more JSON text left to process. + AZ_ERROR_JSON_READER_DONE = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE_JSON, 3), + + // === HTTP error codes === + /// The #az_http_response instance is in an invalid state. + AZ_ERROR_HTTP_INVALID_STATE = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE_HTTP, 1), + + /// HTTP pipeline is malformed. + AZ_ERROR_HTTP_PIPELINE_INVALID_POLICY = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE_HTTP, 2), + + /// Unknown HTTP method verb. + AZ_ERROR_HTTP_INVALID_METHOD_VERB = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE_HTTP, 3), + + /// Authentication failed. + AZ_ERROR_HTTP_AUTHENTICATION_FAILED = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE_HTTP, 4), + + /// HTTP response overflow. + AZ_ERROR_HTTP_RESPONSE_OVERFLOW = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE_HTTP, 5), + + /// Couldn't resolve host. + AZ_ERROR_HTTP_RESPONSE_COULDNT_RESOLVE_HOST = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE_HTTP, 6), + + /// Error while parsing HTTP response header. + AZ_ERROR_HTTP_CORRUPT_RESPONSE_HEADER = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE_HTTP, 7), + + /// There are no more headers within the HTTP response payload. + AZ_ERROR_HTTP_END_OF_HEADERS = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE_HTTP, 8), + + // === HTTP Adapter error codes === + /// Generic error in the HTTP transport adapter implementation. + AZ_ERROR_HTTP_ADAPTER = _az_RESULT_MAKE_ERROR(_az_FACILITY_CORE_HTTP, 9), +}; + +/** + * @brief Checks whether the \p result provided indicates a failure. + * + * @param[in] result Result value to check for failure. + * + * @return `true` if the operation that returned this \p result failed, otherwise return `false`. + */ +AZ_NODISCARD AZ_INLINE bool az_result_failed(az_result result) +{ + return ((uint32_t)result & (uint32_t)_az_ERROR_FLAG) != 0; +} + +/** + * @brief Checks whether the \p result provided indicates a success. + * + * @param[in] result Result value to check for success. + * + * @return `true` if the operation that returned this \p result was successful, otherwise return + * `false`. + */ +AZ_NODISCARD AZ_INLINE bool az_result_succeeded(az_result result) +{ + return !az_result_failed(result); +} + +#include <_az_cfg_suffix.h> + +#endif // _az_RESULT_H diff --git a/src/az_result_internal.h b/src/az_result_internal.h new file mode 100644 index 00000000..1000b3c5 --- /dev/null +++ b/src/az_result_internal.h @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Definition of #az_result related internal helper functions. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_RESULT_INTERNAL_H +#define _az_RESULT_INTERNAL_H + +#include +#include + +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief Convenience macro to return if an operation failed. + */ +#define _az_RETURN_IF_FAILED(exp) \ + do \ + { \ + az_result const _az_result = (exp); \ + if (az_result_failed(_az_result)) \ + { \ + return _az_result; \ + } \ + } while (0) + +/** + * @brief Convenience macro to return if the provided span is not of the expected, required size. + */ +#define _az_RETURN_IF_NOT_ENOUGH_SIZE(span, required_size) \ + do \ + { \ + int32_t const _az_req_sz = (required_size); \ + if (az_span_size(span) < _az_req_sz || _az_req_sz < 0) \ + { \ + return AZ_ERROR_NOT_ENOUGH_SPACE; \ + } \ + } while (0) + +#include <_az_cfg_suffix.h> + +#endif // _az_RESULT_INTERNAL_H diff --git a/src/az_retry_internal.h b/src/az_retry_internal.h new file mode 100644 index 00000000..a361e0ba --- /dev/null +++ b/src/az_retry_internal.h @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#ifndef _az_RETRY_INTERNAL_H +#define _az_RETRY_INTERNAL_H + +#include + +#include <_az_cfg_prefix.h> + +AZ_NODISCARD AZ_INLINE int32_t +_az_retry_calc_delay(int32_t attempt, int32_t retry_delay_msec, int32_t max_retry_delay_msec) +{ + // scale exponentially + int32_t const exponential_retry_after + = retry_delay_msec * (attempt <= 30 ? (int32_t)(1U << (uint32_t)attempt) : INT32_MAX); + + return exponential_retry_after > max_retry_delay_msec ? max_retry_delay_msec + : exponential_retry_after; +} + +#include <_az_cfg_suffix.h> + +#endif // _az_RETRY_INTERNAL_H diff --git a/src/az_span.c b/src/az_span.c new file mode 100644 index 00000000..78cc6a6e --- /dev/null +++ b/src/az_span.c @@ -0,0 +1,1006 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include "az_hex_private.h" +#include "az_span_private.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include <_az_cfg.h> + +// The maximum integer value that can be stored in a double without losing precision (2^53 - 1) +// An IEEE 64-bit double has 52 bits of mantissa +#define _az_MAX_SAFE_INTEGER 9007199254740991 + +#ifndef AZ_NO_PRECONDITION_CHECKING +// Note: If you are modifying this function, make sure to modify the inline version in the az_span.h +// file as well. +AZ_NODISCARD az_span az_span_create(uint8_t* ptr, int32_t size) +{ + // If ptr is not null, then: + // size >= 0 + // Otherwise, if ptr is null, then: + // size == 0 + _az_PRECONDITION((ptr != NULL && size >= 0) || (ptr + (uint32_t)size == 0)); + + return (az_span){ ._internal = { .ptr = ptr, .size = size } }; +} +#endif // AZ_NO_PRECONDITION_CHECKING + +AZ_NODISCARD az_span az_span_create_from_str(char* str) +{ + _az_PRECONDITION_NOT_NULL(str); + + // Avoid passing in null pointer to strlen to avoid memory access violation. + if (str == NULL) + { + return AZ_SPAN_EMPTY; + } + + int32_t const length = (int32_t)strlen(str); + + _az_PRECONDITION(length >= 0); + + return az_span_create((uint8_t*)str, length); +} + +AZ_NODISCARD az_span az_span_slice(az_span span, int32_t start_index, int32_t end_index) +{ + _az_PRECONDITION_VALID_SPAN(span, 0, true); + + // The following set of preconditions validate that: + // 0 <= end_index <= span.size + // And + // 0 <= start_index <= end_index + _az_PRECONDITION_RANGE(0, end_index, az_span_size(span)); + _az_PRECONDITION((uint32_t)start_index <= (uint32_t)end_index); + + return az_span_create(az_span_ptr(span) + start_index, end_index - start_index); +} + +AZ_NODISCARD az_span az_span_slice_to_end(az_span span, int32_t start_index) +{ + return az_span_slice(span, start_index, az_span_size(span)); +} + +AZ_NODISCARD AZ_INLINE uint8_t _az_tolower(uint8_t value) +{ + // This is equivalent to the following but with fewer conditions. + // return 'A' <= value && value <= 'Z' ? value + AZ_ASCII_LOWER_DIF : value; + if ((uint8_t)(int8_t)(value - 'A') <= ('Z' - 'A')) + { + value = (uint8_t)((uint32_t)(value + _az_ASCII_LOWER_DIF) & (uint8_t)UINT8_MAX); + } + return value; +} + +AZ_NODISCARD bool az_span_is_content_equal_ignoring_case(az_span span1, az_span span2) +{ + int32_t const size = az_span_size(span1); + if (size != az_span_size(span2)) + { + return false; + } + for (int32_t i = 0; i < size; ++i) + { + if (_az_tolower(az_span_ptr(span1)[i]) != _az_tolower(az_span_ptr(span2)[i])) + { + return false; + } + } + return true; +} + +AZ_NODISCARD az_result az_span_atou64(az_span source, uint64_t* out_number) +{ + _az_PRECONDITION_VALID_SPAN(source, 1, false); + _az_PRECONDITION_NOT_NULL(out_number); + + int32_t const span_size = az_span_size(source); + + if (span_size < 1) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + // If the first character is not a digit or an optional + sign, return error. + int32_t starting_index = 0; + uint8_t* source_ptr = az_span_ptr(source); + uint8_t next_byte = source_ptr[0]; + + if (!isdigit(next_byte)) + { + // There must be another byte after a sign. + // The loop below checks that it must be a digit. + if (next_byte != '+' || span_size < 2) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + starting_index++; + } + + uint64_t value = 0; + + for (int32_t i = starting_index; i < span_size; ++i) + { + next_byte = source_ptr[i]; + if (!isdigit(next_byte)) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + uint64_t const d = (uint64_t)next_byte - '0'; + + // Check whether the next digit will cause an integer overflow. + // Before actually doing the math below, this is checking whether value * 10 + d > UINT64_MAX. + if ((UINT64_MAX - d) / _az_NUMBER_OF_DECIMAL_VALUES < value) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + value = value * _az_NUMBER_OF_DECIMAL_VALUES + d; + } + + *out_number = value; + return AZ_OK; +} + +AZ_NODISCARD az_result az_span_atou32(az_span source, uint32_t* out_number) +{ + _az_PRECONDITION_VALID_SPAN(source, 1, false); + _az_PRECONDITION_NOT_NULL(out_number); + + int32_t const span_size = az_span_size(source); + + if (span_size < 1) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + // If the first character is not a digit or an optional + sign, return error. + int32_t starting_index = 0; + uint8_t* source_ptr = az_span_ptr(source); + uint8_t next_byte = source_ptr[0]; + + if (!isdigit(next_byte)) + { + // There must be another byte after a sign. + // The loop below checks that it must be a digit. + if (next_byte != '+' || span_size < 2) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + starting_index++; + } + + uint32_t value = 0; + + for (int32_t i = starting_index; i < span_size; ++i) + { + next_byte = source_ptr[i]; + if (!isdigit(next_byte)) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + uint32_t const d = (uint32_t)next_byte - '0'; + + // Check whether the next digit will cause an integer overflow. + // Before actually doing the math below, this is checking whether value * 10 + d > UINT32_MAX. + if ((UINT32_MAX - d) / _az_NUMBER_OF_DECIMAL_VALUES < value) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + value = value * _az_NUMBER_OF_DECIMAL_VALUES + d; + } + + *out_number = value; + return AZ_OK; +} + +AZ_NODISCARD az_result az_span_atoi64(az_span source, int64_t* out_number) +{ + _az_PRECONDITION_VALID_SPAN(source, 1, false); + _az_PRECONDITION_NOT_NULL(out_number); + + int32_t const span_size = az_span_size(source); + + if (span_size < 1) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + // If the first character is not a digit, - sign, or an optional + sign, return error. + int32_t starting_index = 0; + uint8_t* source_ptr = az_span_ptr(source); + uint8_t next_byte = source_ptr[0]; + int64_t sign = 1; + + if (!isdigit(next_byte)) + { + // There must be another byte after a sign. + // The loop below checks that it must be a digit. + if (next_byte != '+') + { + if (next_byte != '-') + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + sign = -1; + } + if (span_size < 2) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + starting_index++; + } + + // if sign < 0, (-1 * sign + 1) / 2 = 1 + // else, (-1 * sign + 1) / 2 = 0 + // This is necessary to correctly account for the fact that the absolute value of INT64_MIN is 1 + // more than than the absolute value of INT64_MAX. + uint64_t sign_factor = (uint64_t)(-1 * sign + 1) / 2; + + // Using unsigned int while parsing to account for potential overflow. + uint64_t value = 0; + + for (int32_t i = starting_index; i < span_size; ++i) + { + next_byte = source_ptr[i]; + if (!isdigit(next_byte)) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + uint64_t const d = (uint64_t)next_byte - '0'; + + // Check whether the next digit will cause an integer overflow. + // Before actually doing the math below, this is checking whether value * 10 + d > INT64_MAX, or + // in the case of negative numbers, checking whether value * 10 + d > INT64_MAX + 1. + if ((uint64_t)(INT64_MAX - d + sign_factor) / _az_NUMBER_OF_DECIMAL_VALUES < value) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + value = value * _az_NUMBER_OF_DECIMAL_VALUES + d; + } + + *out_number = (int64_t)value * sign; + return AZ_OK; +} + +AZ_NODISCARD az_result az_span_atoi32(az_span source, int32_t* out_number) +{ + _az_PRECONDITION_VALID_SPAN(source, 1, false); + _az_PRECONDITION_NOT_NULL(out_number); + + int32_t const span_size = az_span_size(source); + + if (span_size < 1) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + // If the first character is not a digit, - sign, or an optional + sign, return error. + int32_t starting_index = 0; + uint8_t* source_ptr = az_span_ptr(source); + uint8_t next_byte = source_ptr[0]; + int32_t sign = 1; + + if (!isdigit(next_byte)) + { + // There must be another byte after a sign. + // The loop below checks that it must be a digit. + if (next_byte != '+') + { + if (next_byte != '-') + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + sign = -1; + } + if (span_size < 2) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + starting_index++; + } + + // if sign < 0, (-1 * sign + 1) / 2 = 1 + // else, (-1 * sign + 1) / 2 = 0 + // This is necessary to correctly account for the fact that the absolute value of INT32_MIN is 1 + // more than than the absolute value of INT32_MAX. + uint32_t sign_factor = (uint32_t)(-1 * sign + 1) / 2; + + // Using unsigned int while parsing to account for potential overflow. + uint32_t value = 0; + + for (int32_t i = starting_index; i < span_size; ++i) + { + next_byte = source_ptr[i]; + if (!isdigit(next_byte)) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + uint32_t const d = (uint32_t)next_byte - '0'; + + // Check whether the next digit will cause an integer overflow. + // Before actually doing the math below, this is checking whether value * 10 + d > INT32_MAX, or + // in the case of negative numbers, checking whether value * 10 + d > INT32_MAX + 1. + if ((uint32_t)(INT32_MAX - d + sign_factor) / _az_NUMBER_OF_DECIMAL_VALUES < value) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + value = value * _az_NUMBER_OF_DECIMAL_VALUES + d; + } + + *out_number = (int32_t)value * sign; + return AZ_OK; +} + +static bool _is_valid_start_of_double(uint8_t first_byte) +{ + // ".123", " 123", "nan", or "inf" are considered invalid + bool result = isdigit(first_byte) || first_byte == '+' || first_byte == '-'; + + return result; +} + +// Disable the following warning just for this particular use case. +// C4996: 'sscanf': This function or variable may be unsafe. Consider using sscanf_s instead. +// C4710: 'sscanf': function not inlined +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4996) +#pragma warning(disable : 4710) +#endif + +AZ_NODISCARD az_result az_span_atod(az_span source, double* out_number) +{ + _az_PRECONDITION_VALID_SPAN(source, 1, false); + _az_PRECONDITION_NOT_NULL(out_number); + + int32_t size = az_span_size(source); + + _az_PRECONDITION_RANGE(1, size, _az_MAX_SIZE_FOR_PARSING_DOUBLE); + + // This check is necessary to prevent sscanf from reading bytes past the end of the span, when the + // span might contain whitespace or other invalid bytes at the start. + uint8_t* source_ptr = az_span_ptr(source); + if (size < 1 || !_is_valid_start_of_double(source_ptr[0])) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + + // Stack based string to allow thread-safe mutation. + // The length is 8 to allow space for the null-terminating character. + // NOLINTNEXTLINE(readability-magic-numbers, cppcoreguidelines-avoid-magic-numbers) + char format[8] = "%00lf%n"; + + // Starting at 1 to skip the '%' character + format[1] = (char)((size / _az_NUMBER_OF_DECIMAL_VALUES) + '0'); + format[2] = (char)((size % _az_NUMBER_OF_DECIMAL_VALUES) + '0'); + + int32_t chars_consumed = 0; + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + int32_t n = sscanf((char*)source_ptr, format, out_number, &chars_consumed); + + // Success if the entire source was consumed by sscanf and it set the out_number argument. + return (size == chars_consumed && n == 1 && _az_isfinite(*out_number)) ? AZ_OK + : AZ_ERROR_UNEXPECTED_CHAR; +} + +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +AZ_NODISCARD int32_t az_span_find(az_span source, az_span target) +{ + /* This function implements the Naive string-search algorithm. + * The rationale to use this algorithm instead of other potentially more + * performing ones (Rabin-Karp, e.g.) is due to no additional space needed. + * The logic: + * 1. The function will look into each position of `source` if it contains the same value as the + * first position of `target`. + * 2. If it does, it could be that the next bytes in `source` are a perfect match of the remaining + * bytes of `target`. + * 3. Being so, it loops through the remaining bytes of `target` and see if they match exactly the + * next bytes of `source`. + * 4. If the loop gets to the end (all bytes of `target` are evaluated), it means `target` indeed + * occurs in that position of `source`. + * 5. If the loop gets interrupted before cruising through the entire `target`, the function must + * go back to step 1. from the next position in `source`. + * The loop in 5. gets interrupted if + * - a byte in `target` is different than `source`, in the expected corresponding position; + * - the loop has reached the end of `source` (and there are still remaining bytes of `target` + * to be checked). + */ + + int32_t source_size = az_span_size(source); + int32_t target_size = az_span_size(target); + const int32_t target_not_found = -1; + + if (target_size == 0) + { + return 0; + } + + if (source_size < target_size) + { + return target_not_found; + } + + uint8_t* source_ptr = az_span_ptr(source); + uint8_t* target_ptr = az_span_ptr(target); + + // This loop traverses `source` position by position (step 1.) + for (int32_t i = 0; i < (source_size - target_size + 1); i++) + { + // This is the check done in step 1. above. + if (source_ptr[i] == target_ptr[0]) + { + // The condition in step 2. has been satisfied. + int32_t j = 1; + // This is the loop defined in step 3. + // The loop must be broken if it reaches the ends of `target` (step 3.) OR `source` + // (step 5.). + for (; j < target_size && (i + j) < source_size; j++) + { + // Condition defined in step 5. + if (source_ptr[i + j] != target_ptr[j]) + { + break; + } + } + + if (j == target_size) + { + // All bytes in `target` have been checked and matched the corresponding bytes in `source` + // (from the start point `i`), so this is indeed an instance of `target` in that position + // of `source` (step 4.). + + return i; + } + } + } + + // If the function hasn't returned before, all positions + // of `source` have been evaluated but `target` could not be found. + return target_not_found; +} + +az_span az_span_copy(az_span destination, az_span source) +{ + int32_t src_size = az_span_size(source); + + _az_PRECONDITION_VALID_SPAN(destination, src_size, false); + + if (src_size == 0) + { + return destination; + } + + // Even though the contract of this function is that the destination must be larger than source, + // cap the data move if the source is too large, to avoid memory corruption. + int32_t dest_size = az_span_size(destination); + if (src_size > dest_size) + { + src_size = dest_size; + } + + uint8_t* ptr = az_span_ptr(destination); + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + memmove((void*)ptr, (void const*)az_span_ptr(source), (size_t)src_size); + + return az_span_slice_to_end(destination, src_size); +} + +az_span az_span_copy_u8(az_span destination, uint8_t byte) +{ + _az_PRECONDITION_VALID_SPAN(destination, 1, false); + + // Even though the contract of the function is that the destination must be at least 1 byte large, + // no-op if it is empty to avoid memory corruption. + int32_t dest_size = az_span_size(destination); + if (dest_size < 1) + { + return destination; + } + + uint8_t* dst_ptr = az_span_ptr(destination); + dst_ptr[0] = byte; + return az_span_create(dst_ptr + 1, dest_size - 1); +} + +void az_span_to_str(char* destination, int32_t destination_max_size, az_span source) +{ + _az_PRECONDITION_NOT_NULL(destination); + _az_PRECONDITION(destination_max_size > 0); + + // Implementations of memmove generally do the right thing when number of bytes to move is 0, even + // if the ptr is null, but given the behavior is documented to be undefined, we disallow it as a + // precondition. + _az_PRECONDITION_VALID_SPAN(source, 0, false); + + int32_t size_to_write = az_span_size(source); + + _az_PRECONDITION(size_to_write < destination_max_size); + + // Even though the contract of this function is that the destination_max_size must be larger than + // source to be able to copy all of the source to the char buffer including an extra null + // terminating character, cap the data move if the source is too large, to avoid memory + // corruption. + if (size_to_write >= destination_max_size) + { + // Leave enough space for the null terminator. + size_to_write = destination_max_size - 1; + + // If destination_max_size was 0, we don't want size_to_write to be negative and + // corrupt data before the destination pointer. + if (size_to_write < 0) + { + size_to_write = 0; + } + } + + _az_PRECONDITION(size_to_write >= 0); + + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + memmove((void*)destination, (void const*)az_span_ptr(source), (size_t)size_to_write); + destination[size_to_write] = 0; +} + +AZ_INLINE uint8_t _az_decimal_to_ascii(uint8_t d) +{ + return (uint8_t)((uint32_t)('0' + d) & (uint8_t)UINT8_MAX); +} + +static AZ_NODISCARD az_result _az_span_builder_append_uint64(az_span* ref_span, uint64_t n) +{ + _az_RETURN_IF_NOT_ENOUGH_SIZE(*ref_span, 1); + + if (n == 0) + { + *ref_span = az_span_copy_u8(*ref_span, '0'); + return AZ_OK; + } + + uint64_t div = _az_SMALLEST_20_DIGIT_NUMBER; + uint64_t nn = n; + int32_t digit_count = _az_MAX_SIZE_FOR_UINT64; + while (nn / div == 0) + { + div /= _az_NUMBER_OF_DECIMAL_VALUES; + digit_count--; + } + + _az_RETURN_IF_NOT_ENOUGH_SIZE(*ref_span, digit_count); + + while (div > 1) + { + uint8_t value_to_append = _az_decimal_to_ascii((uint8_t)(nn / div)); + *ref_span = az_span_copy_u8(*ref_span, value_to_append); + nn %= div; + div /= _az_NUMBER_OF_DECIMAL_VALUES; + } + uint8_t value_to_append = _az_decimal_to_ascii((uint8_t)nn); + *ref_span = az_span_copy_u8(*ref_span, value_to_append); + return AZ_OK; +} + +AZ_NODISCARD az_result az_span_u64toa(az_span destination, uint64_t source, az_span* out_span) +{ + _az_PRECONDITION_VALID_SPAN(destination, 0, false); + _az_PRECONDITION_NOT_NULL(out_span); + *out_span = destination; + + return _az_span_builder_append_uint64(out_span, source); +} + +AZ_NODISCARD az_result az_span_i64toa(az_span destination, int64_t source, az_span* out_span) +{ + _az_PRECONDITION_VALID_SPAN(destination, 0, false); + _az_PRECONDITION_NOT_NULL(out_span); + + if (source < 0) + { + _az_RETURN_IF_NOT_ENOUGH_SIZE(destination, 1); + *out_span = az_span_copy_u8(destination, '-'); + return _az_span_builder_append_uint64(out_span, (uint64_t)-source); + } + + // make out_span point to destination before trying to write on it (might be an empty az_span or + // pointing else where) + *out_span = destination; + return _az_span_builder_append_uint64(out_span, (uint64_t)source); +} + +static AZ_NODISCARD az_result +_az_span_builder_append_u32toa(az_span destination, uint32_t n, az_span* out_span) +{ + _az_RETURN_IF_NOT_ENOUGH_SIZE(destination, 1); + + if (n == 0) + { + *out_span = az_span_copy_u8(destination, '0'); + return AZ_OK; + } + + uint32_t div = _az_SMALLEST_10_DIGIT_NUMBER; + uint32_t nn = n; + int32_t digit_count = _az_MAX_SIZE_FOR_UINT32; + while (nn / div == 0) + { + div /= _az_NUMBER_OF_DECIMAL_VALUES; + digit_count--; + } + + _az_RETURN_IF_NOT_ENOUGH_SIZE(destination, digit_count); + + *out_span = destination; + + while (div > 1) + { + uint8_t value_to_append = _az_decimal_to_ascii((uint8_t)(nn / div)); + *out_span = az_span_copy_u8(*out_span, value_to_append); + + nn %= div; + div /= _az_NUMBER_OF_DECIMAL_VALUES; + } + + uint8_t value_to_append = _az_decimal_to_ascii((uint8_t)nn); + *out_span = az_span_copy_u8(*out_span, value_to_append); + return AZ_OK; +} + +AZ_NODISCARD az_result az_span_u32toa(az_span destination, uint32_t source, az_span* out_span) +{ + _az_PRECONDITION_VALID_SPAN(destination, 0, false); + _az_PRECONDITION_NOT_NULL(out_span); + return _az_span_builder_append_u32toa(destination, source, out_span); +} + +AZ_NODISCARD az_result az_span_i32toa(az_span destination, int32_t source, az_span* out_span) +{ + _az_PRECONDITION_VALID_SPAN(destination, 0, false); + _az_PRECONDITION_NOT_NULL(out_span); + + *out_span = destination; + + if (source < 0) + { + _az_RETURN_IF_NOT_ENOUGH_SIZE(*out_span, 1); + *out_span = az_span_copy_u8(*out_span, '-'); + source = -source; + } + + return _az_span_builder_append_u32toa(*out_span, (uint32_t)source, out_span); +} + +AZ_NODISCARD az_result +az_span_dtoa(az_span destination, double source, int32_t fractional_digits, az_span* out_span) +{ + _az_PRECONDITION_VALID_SPAN(destination, 0, false); + // Inputs that are either positive or negative infinity, or not a number, are not supported. + _az_PRECONDITION(_az_isfinite(source)); + _az_PRECONDITION_RANGE(0, fractional_digits, _az_MAX_SUPPORTED_FRACTIONAL_DIGITS); + _az_PRECONDITION_NOT_NULL(out_span); + + *out_span = destination; + + // The input is either positive or negative infinity, or not a number. + if (!_az_isfinite(source)) + { + return AZ_ERROR_NOT_SUPPORTED; + } + + if (source < 0) + { + _az_RETURN_IF_NOT_ENOUGH_SIZE(*out_span, 1); + *out_span = az_span_copy_u8(*out_span, '-'); + source = -source; + } + + double integer_part = 0; + double after_decimal_part = modf(source, &integer_part); + + if (integer_part > _az_MAX_SAFE_INTEGER) + { + return AZ_ERROR_NOT_SUPPORTED; + } + + // The double to uint64_t cast should be safe without loss of precision. + // Append the integer part. + _az_RETURN_IF_FAILED(_az_span_builder_append_uint64(out_span, (uint64_t)integer_part)); + + // Only print decimal digits if the user asked for at least one to be printed. + // Or if the decimal part is non-zero. + if (fractional_digits <= 0) + { + return AZ_OK; + } + + // Clamp the fractional digits to the supported maximum value of 15. + if (fractional_digits > _az_MAX_SUPPORTED_FRACTIONAL_DIGITS) + { + fractional_digits = _az_MAX_SUPPORTED_FRACTIONAL_DIGITS; + } + + int32_t leading_zeros = 0; + double shifted_fractional = after_decimal_part; + for (int32_t d = 0; d < fractional_digits; d++) + { + shifted_fractional *= _az_NUMBER_OF_DECIMAL_VALUES; + + // Any decimal component that is less than 0.1, when multiplied by 10, will be less than 1, + // which indicate a leading zero is present after the decimal point. For example, the decimal + // part could be 0.00, 0.09, 0.00010, etc. + if (shifted_fractional < 1) + { + leading_zeros++; + continue; + } + } + + double shifted_fractional_integer_part = 0; + double unused = modf(shifted_fractional, &shifted_fractional_integer_part); + (void)unused; + + // Since the maximum allowed fractional_digits is 15, this is guaranteed to be true. + _az_PRECONDITION(shifted_fractional_integer_part <= _az_MAX_SAFE_INTEGER); + + // The double to uint64_t cast should be safe without loss of precision. + uint64_t fractional_part = (uint64_t)shifted_fractional_integer_part; + + // If there is no fractional part (at least within the number of fractional digits the user + // specified), or if they were all non-significant zeros, don't print the decimal point or any + // trailing zeros. + if (fractional_part == 0) + { + return AZ_OK; + } + + // Remove trailing zeros of the fraction part that don't need to be printed since they aren't + // significant. + while (fractional_part % _az_NUMBER_OF_DECIMAL_VALUES == 0) + { + fractional_part /= _az_NUMBER_OF_DECIMAL_VALUES; + } + + _az_RETURN_IF_NOT_ENOUGH_SIZE(*out_span, 1 + leading_zeros); + *out_span = az_span_copy_u8(*out_span, '.'); + + for (int32_t z = 0; z < leading_zeros; z++) + { + *out_span = az_span_copy_u8(*out_span, '0'); + } + + // Append the fractional part. + return _az_span_builder_append_uint64(out_span, fractional_part); +} + +// TODO: pass az_span by value +AZ_NODISCARD az_result _az_is_expected_span(az_span* ref_span, az_span expected) +{ + int32_t expected_size = az_span_size(expected); + + // EOF because ref_span is smaller than the expected span + if (expected_size > az_span_size(*ref_span)) + { + return AZ_ERROR_UNEXPECTED_END; + } + + az_span actual_span = az_span_slice(*ref_span, 0, expected_size); + + if (!az_span_is_content_equal(actual_span, expected)) + { + return AZ_ERROR_UNEXPECTED_CHAR; + } + // move reader after the expected span (means it was parsed as expected) + *ref_span = az_span_slice_to_end(*ref_span, expected_size); + + return AZ_OK; +} + +AZ_NODISCARD az_span _az_span_trim_whitespace(az_span source) +{ + // Trim from end after trim from start + return _az_span_trim_whitespace_from_end(_az_span_trim_whitespace_from_start(source)); +} + +AZ_NODISCARD AZ_INLINE bool _az_is_whitespace(uint8_t c) +{ + switch (c) + { + case ' ': + case '\t': + case '\n': + case '\r': + return true; + default: + return false; + } +} + +typedef enum +{ + LEFT = 0, + RIGHT = 1, +} az_span_trim_side; + +// Return a trim az_span. Depending on arg side, function will trim left of right +AZ_NODISCARD static az_span _az_span_trim_side(az_span source, az_span_trim_side side) +{ + int32_t increment = 1; + uint8_t* source_ptr = az_span_ptr(source); + int32_t source_size = az_span_size(source); + + if (side == RIGHT) + { + increment = -1; // Set increment to be decremental for moving ptr + source_ptr += ((size_t)source_size - 1); // Set initial position to the end + } + + // loop source, just to make sure staying within the size range + int32_t index = 0; + for (; index < source_size; index++) + { + if (!_az_is_whitespace(*source_ptr)) + { + break; + } + // update ptr to next position + source_ptr += increment; + } + + // return the slice depending on side + if (side == RIGHT) + { + // calculate index from right. + index = source_size - index; + return az_span_slice(source, 0, index); + } + + return az_span_slice_to_end(source, index); // worst case index would be source_size +} + +AZ_NODISCARD az_span _az_span_trim_whitespace_from_start(az_span source) +{ + return _az_span_trim_side(source, LEFT); +} + +AZ_NODISCARD az_span _az_span_trim_whitespace_from_end(az_span source) +{ + return _az_span_trim_side(source, RIGHT); +} + +// [0-9] +AZ_NODISCARD AZ_INLINE bool _az_span_is_byte_digit(uint8_t c) { return '0' <= c && c <= '9'; } + +// [A-Za-z] +AZ_NODISCARD AZ_INLINE bool _az_span_is_byte_letter(uint8_t c) +{ + // This is equivalent to ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') + // This works because upper case and lower case letters are 0x20 away from each other. + return ((uint32_t)(c - 'A') & ~(uint32_t)_az_ASCII_SPACE_CHARACTER) <= 'Z' - 'A'; +} + +AZ_NODISCARD AZ_INLINE bool _az_span_url_should_encode(uint8_t c) +{ + switch (c) + { + case '-': + case '_': + case '.': + case '~': + return false; + default: + return !(_az_span_is_byte_digit(c) || _az_span_is_byte_letter(c)); + } +} + +AZ_NODISCARD int32_t _az_span_url_encode_calc_length(az_span source) +{ + _az_PRECONDITION_VALID_SPAN(source, 0, true); + // Trying to calculate the number of bytes to encode more than INT32_MAX / 3 might overflow an + // int32 and return an erroneous number back. + _az_PRECONDITION_RANGE(0, az_span_size(source), INT32_MAX / 3); + + int32_t const source_size = az_span_size(source); + uint8_t const* const src_ptr = az_span_ptr(source); + + int32_t encoded_length = source_size; + for (int32_t i = 0; i < source_size; i++) + { + uint8_t c = src_ptr[i]; + if (_az_span_url_should_encode(c)) + { + // Adding '%' plus 2 digits (minus 1 as original symbol is counted as 1) + encoded_length += 2; + } + } + + // If source_size is 0, this will return 0. + return encoded_length; +} + +AZ_NODISCARD az_result _az_span_url_encode(az_span destination, az_span source, int32_t* out_length) +{ + _az_PRECONDITION_NOT_NULL(out_length); + _az_PRECONDITION_VALID_SPAN(source, 0, true); + + int32_t const source_size = az_span_size(source); + _az_PRECONDITION_VALID_SPAN(destination, source_size, false); + + _az_PRECONDITION_NO_OVERLAP_SPANS(destination, source); + + uint8_t* const dest_begin = az_span_ptr(destination); + uint8_t* const dest_end = dest_begin + az_span_size(destination); + + uint8_t* const src_ptr = az_span_ptr(source); + uint8_t* dest_ptr = dest_begin; + + for (int32_t i = 0; i < source_size; i++) + { + uint8_t c = src_ptr[i]; + if (!_az_span_url_should_encode(c)) + { + if (dest_ptr >= dest_end) + { + *out_length = 0; + return AZ_ERROR_NOT_ENOUGH_SPACE; + } + + *dest_ptr = c; + ++dest_ptr; + } + else + { + if (dest_ptr >= dest_end - 2) + { + *out_length = 0; + return AZ_ERROR_NOT_ENOUGH_SPACE; + } + + dest_ptr[0] = '%'; + dest_ptr[1] = _az_number_to_upper_hex(c >> 4U); + dest_ptr[2] = _az_number_to_upper_hex(c & (uint32_t)_az_LARGEST_HEX_VALUE); + dest_ptr += 3; + } + } + + *out_length = (int32_t)(dest_ptr - dest_begin); + return AZ_OK; +} + +az_span _az_span_token( + az_span source, + az_span delimiter, + az_span* out_remainder, + int32_t* out_index) +{ + _az_PRECONDITION_VALID_SPAN(source, 1, false); + _az_PRECONDITION_VALID_SPAN(delimiter, 1, false); + _az_PRECONDITION_NOT_NULL(out_remainder); + + *out_index = az_span_find(source, delimiter); + + if (*out_index != -1) + { + *out_remainder + = az_span_slice(source, *out_index + az_span_size(delimiter), az_span_size(source)); + + return az_span_slice(source, 0, *out_index); + } + + *out_remainder = AZ_SPAN_EMPTY; + return source; +} diff --git a/src/az_span.h b/src/az_span.h new file mode 100644 index 00000000..c6956466 --- /dev/null +++ b/src/az_span.h @@ -0,0 +1,530 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief An #az_span represents a contiguous byte buffer and is used for string manipulations, + * HTTP requests/responses, reading/writing JSON payloads, and more. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_SPAN_H +#define _az_SPAN_H + +#include + +#include +#include +#include +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief Represents a "view" over a byte buffer that represents a contiguous region of memory. It + * contains a pointer to the start of the byte buffer and the buffer's size. + */ +typedef struct +{ + struct + { + uint8_t* ptr; + int32_t size; // size must be >= 0 + } _internal; +} az_span; + +/******************************** SPAN GETTERS */ + +/** + * @brief Returns the #az_span byte buffer's starting memory address. + * @param[in] span The #az_span whose starting memory address to return. + * @return Starting memory address of \p span buffer. + */ +AZ_NODISCARD AZ_INLINE uint8_t* az_span_ptr(az_span span) { return span._internal.ptr; } + +/** + * @brief Returns the number of bytes within the #az_span. + * @param[in] span The #az_span whose size to return. + * @return Size of \p span buffer. + */ +AZ_NODISCARD AZ_INLINE int32_t az_span_size(az_span span) { return span._internal.size; } + +/******************************** CONSTRUCTORS */ + +/** + * @brief Returns an #az_span over a byte buffer. + * + * @param[in] ptr The memory address of the first byte in the byte buffer. + * @param[in] size The total number of bytes in the byte buffer. + * + * @return The "view" over the byte buffer. + */ +// Note: If you are modifying this function, make sure to modify the non-inline version in the +// az_span.c file as well, and the _az_ version right below. +#ifdef AZ_NO_PRECONDITION_CHECKING +AZ_NODISCARD AZ_INLINE az_span az_span_create(uint8_t* ptr, int32_t size) +{ + return (az_span){ ._internal = { .ptr = ptr, .size = size } }; +} +#else +AZ_NODISCARD az_span az_span_create(uint8_t* ptr, int32_t size); +#endif // AZ_NO_PRECONDITION_CHECKING + +/** + * @brief An empty #az_span. + * + * @remark There is no guarantee that the pointer backing this span will be `NULL` and the caller + * shouldn't rely on it. However, the size will be 0. + */ +#define AZ_SPAN_EMPTY \ + (az_span) \ + { \ + ._internal = {.ptr = NULL, .size = 0 } \ + } + +// Returns the size (in bytes) of a literal string. +// Note: Concatenating "" to S produces a compiler error if S is not a literal string +// The stored string's length does not include the \0 terminator. +#define _az_STRING_LITERAL_LEN(S) (sizeof(S "") - 1) + +/** + * @brief Returns a literal #az_span over a literal string. + * The size of the #az_span is equal to the length of the string. + * + * For example: + * + * `static const az_span hw = AZ_SPAN_LITERAL_FROM_STR("Hello world");` + * + * @remarks An empty ("") literal string results in an #az_span with size set to 0. + */ +#define AZ_SPAN_LITERAL_FROM_STR(STRING_LITERAL) \ + { \ + ._internal = { \ + .ptr = (uint8_t*)(STRING_LITERAL), \ + .size = _az_STRING_LITERAL_LEN(STRING_LITERAL), \ + }, \ + } + +/** + * @brief Returns an #az_span expression over a literal string. + * + * For example: + * + * `some_function(AZ_SPAN_FROM_STR("Hello world"));` + * + * where + * + * `void some_function(const az_span span);` + * + */ +#define AZ_SPAN_FROM_STR(STRING_LITERAL) (az_span) AZ_SPAN_LITERAL_FROM_STR(STRING_LITERAL) + +// Returns 1 if the address of the array is equal to the address of its 1st element. +// Returns 0 for anything that is not an array (for example any arbitrary pointer). +#define _az_IS_ARRAY(array) (((void*)&(array)) == ((void*)(&(array)[0]))) + +// Returns 1 if the element size of the array is 1 (which is only true for byte arrays such as +// uint8_t[] and char[]). +// Returns 0 for any other element size (for example int32_t[]). +#define _az_IS_BYTE_ARRAY(array) ((sizeof((array)[0]) == 1) && _az_IS_ARRAY(array)) + +/** + * @brief Returns an #az_span expression over an uninitialized byte buffer. + * + * For example: + * + * `uint8_t buffer[1024];` + * + * `some_function(AZ_SPAN_FROM_BUFFER(buffer)); // Size = 1024` + * + * @remarks BYTE_BUFFER MUST be an array defined like `uint8_t buffer[10];` and not `uint8_t* + * buffer` + */ +// Force a division by 0 that gets detected by compilers for anything that isn't a byte array. +#define AZ_SPAN_FROM_BUFFER(BYTE_BUFFER) \ + az_span_create( \ + (uint8_t*)(BYTE_BUFFER), (sizeof(BYTE_BUFFER) / (_az_IS_BYTE_ARRAY(BYTE_BUFFER) ? 1 : 0))) + +/** + * @brief Returns an #az_span from a 0-terminated array of bytes (chars). + * + * @param[in] str The pointer to the 0-terminated array of bytes (chars). + * + * @return An #az_span over the byte buffer where the size is set to the string's length not + * including the `\0` terminator. + */ +AZ_NODISCARD az_span az_span_create_from_str(char* str); + +/****************************** SPAN MANIPULATION */ + +/** + * @brief Returns a new #az_span which is a sub-span of the specified \p span. + * + * @param[in] span The original #az_span. + * @param[in] start_index An index into the original #az_span indicating where the returned #az_span + * will start. + * @param[in] end_index An index into the original #az_span indicating where the returned #az_span + * should stop. The byte at the end_index is NOT included in the returned #az_span. + * + * @return An #az_span into a portion (from \p start_index to \p end_index - 1) of the original + * #az_span. + */ +AZ_NODISCARD az_span az_span_slice(az_span span, int32_t start_index, int32_t end_index); + +/** + * @brief Returns a new #az_span which is a sub-span of the specified \p span. + * + * @param[in] span The original #az_span. + * @param[in] start_index An index into the original #az_span indicating where the returned #az_span + * will start. + * + * @return An #az_span into a portion (from \p start_index to the size) of the original + * #az_span. + */ +AZ_NODISCARD az_span az_span_slice_to_end(az_span span, int32_t start_index); + +/** + * @brief Determines whether two spans are equal by comparing their bytes. + * + * @param[in] span1 The first #az_span to compare. + * @param[in] span2 The second #az_span to compare. + * + * @return `true` if the sizes of both spans are identical and the bytes in both spans are + * also identical. Otherwise, `false`. + */ +AZ_NODISCARD AZ_INLINE bool az_span_is_content_equal(az_span span1, az_span span2) +{ + int32_t span1_size = az_span_size(span1); + int32_t span2_size = az_span_size(span2); + + // Make sure to avoid passing a null pointer to memcmp, which is considered undefined. + // We assume that if the size is non-zero, then the pointer can't be null. + if (span1_size == 0) + { + // Two empty spans are considered equal, even if their pointers are different. + return span2_size == 0; + } + + // If span2_size == 0, then the first condition which compares sizes will be false, since we + // checked the size of span1 above. And due to short-circuiting we won't be calling memcmp anyway. + // Therefore, we don't need to check for that explicitly. + return span1_size == span2_size + && memcmp(az_span_ptr(span1), az_span_ptr(span2), (size_t)span1_size) == 0; +} + +/** + * @brief Determines whether two spans are equal by comparing their characters, except for casing. + * + * @param[in] span1 The first #az_span to compare. + * @param[in] span2 The second #az_span to compare. + * + * @return `true` if the sizes of both spans are identical and the ASCII characters in both + * spans are also identical, except for casing. + * + * @remarks This function assumes the bytes in both spans are ASCII characters. + */ +AZ_NODISCARD bool az_span_is_content_equal_ignoring_case(az_span span1, az_span span2); + +/** + * @brief Copies a \p source #az_span containing a string (that is not 0-terminated) to a \p + destination char buffer and appends the 0-terminating byte. + * + * @param destination A pointer to a buffer where the string should be copied into. + * @param[in] destination_max_size The maximum available space within the buffer referred to by + * \p destination. + * @param[in] source The #az_span containing the not-0-terminated string to copy into \p + destination. + * + * @remarks The buffer referred to by \p destination must have a size that is at least 1 byte bigger + * than the \p source #az_span for the \p destination string to be zero-terminated. + * Content is copied from the \p source buffer and then `\0` is added at the end. + */ +void az_span_to_str(char* destination, int32_t destination_max_size, az_span source); + +/** + * @brief Searches for \p target in \p source, returning an #az_span within \p source if it finds + * it. + * + * @param[in] source The #az_span with the content to be searched on. + * @param[in] target The #az_span containing the tokens to be searched within \p source. + * + * @return The position of \p target in \p source if \p source contains the \p target within it. + * @retval 0 \p target is empty (if its size is equal zero). + * @retval -1 \p target is not found in `source` OR \p source is empty (if its size is zero) and \p + * target is non-empty. + * @retval >=0 The position of \p target in \p source. + */ +AZ_NODISCARD int32_t az_span_find(az_span source, az_span target); + +/****************************** SPAN COPYING */ + +/** + * @brief Copies the content of the \p source #az_span to the \p destination #az_span. + * + * @param destination The #az_span whose bytes will be replaced by the bytes from \p source. + * @param[in] source The #az_span containing the bytes to copy to the destination. + * + * @return An #az_span that is a slice of the \p destination #az_span (i.e. the remainder) after the + * source bytes have been copied. + * + * @remarks This function assumes that the \p destination has a large enough size to hold the \p + * source. + * + * @remarks This function copies all of \p source into the \p destination even if they overlap. + * @remarks If \p source is an empty #az_span or #AZ_SPAN_EMPTY, this function will just return + * \p destination. + */ +az_span az_span_copy(az_span destination, az_span source); + +/** + * @brief Copies the `uint8_t` \p byte to the \p destination at its 0-th index. + * + * @param destination The #az_span where the byte should be copied to. + * @param[in] byte The `uint8_t` to copy into the \p destination span. + * + * @return An #az_span that is a slice of the \p destination #az_span (i.e. the remainder) after the + * \p byte has been copied. + * + * @remarks The function assumes that the \p destination has a large enough size to hold one more + * byte. + */ +az_span az_span_copy_u8(az_span destination, uint8_t byte); + +/** + * @brief Fills all the bytes of the \p destination #az_span with the specified value. + * + * @param destination The #az_span whose bytes will be set to \p value. + * @param[in] value The byte to be replicated within the destination #az_span. + */ +AZ_INLINE void az_span_fill(az_span destination, uint8_t value) +{ + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + memset(az_span_ptr(destination), value, (size_t)az_span_size(destination)); +} + +/****************************** SPAN PARSING AND FORMATTING */ + +/** + * @brief Parses an #az_span containing ASCII digits into a `uint64_t` number. + * + * @param[in] source The #az_span containing the ASCII digits to be parsed. + * @param[out] out_number The pointer to the variable that is to receive the number. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_UNEXPECTED_CHAR A non-ASCII digit is found within the span or the \p source + * contains a number that would overflow or underflow `uint64_t`. + */ +AZ_NODISCARD az_result az_span_atou64(az_span source, uint64_t* out_number); + +/** + * @brief Parses an #az_span containing ASCII digits into an `int64_t` number. + * + * @param[in] source The #az_span containing the ASCII digits to be parsed. + * @param[out] out_number The pointer to the variable that is to receive the number. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_UNEXPECTED_CHAR A non-ASCII digit is found within the span or the \p source + * contains a number that would overflow or underflow `int64_t`. + */ +AZ_NODISCARD az_result az_span_atoi64(az_span source, int64_t* out_number); + +/** + * @brief Parses an #az_span containing ASCII digits into a `uint32_t` number. + * + * @param[in] source The #az_span containing the ASCII digits to be parsed. + * @param[out] out_number The pointer to the variable that is to receive the number. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_UNEXPECTED_CHAR A non-ASCII digit is found within the span or the \p source + * contains a number that would overflow or underflow `uint32_t`. + */ +AZ_NODISCARD az_result az_span_atou32(az_span source, uint32_t* out_number); + +/** + * @brief Parses an #az_span containing ASCII digits into an `int32_t` number. + * + * @param[in] source The #az_span containing the ASCII digits to be parsed. + * @param[out] out_number The pointer to the variable that is to receive the number. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_UNEXPECTED_CHAR A non-ASCII digit is found within the span or if the \p source + * contains a number that would overflow or underflow `int32_t`. + */ +AZ_NODISCARD az_result az_span_atoi32(az_span source, int32_t* out_number); + +/** + * @brief Parses an #az_span containing ASCII digits into a `double` number. + * + * @param[in] source The #az_span containing the ASCII digits to be parsed. + * @param[out] out_number The pointer to the variable that is to receive the number. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_UNEXPECTED_CHAR A non-ASCII digit or an invalid character is found within the + * span, or the resulting \p out_number wouldn't be a finite `double` number. + * + * @remark The #az_span being parsed must contain a number that is finite. Values such as `NaN`, + * `INFINITY`, and those that would overflow a `double` to `+/-inf` are not allowed. + */ +AZ_NODISCARD az_result az_span_atod(az_span source, double* out_number); + +/** + * @brief Converts an `int32_t` into its digit characters (base 10) and copies them to the \p + * destination #az_span starting at its 0-th index. + * + * @param destination The #az_span where the bytes should be copied to. + * @param[in] source The `int32_t` whose number is copied to the \p destination #az_span as ASCII + * digits. + * @param[out] out_span A pointer to an #az_span that receives the remainder of the \p destination + * #az_span after the `int32_t` has been copied. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The \p destination is not big enough to contain the copied + * bytes. + */ +AZ_NODISCARD az_result az_span_i32toa(az_span destination, int32_t source, az_span* out_span); + +/** + * @brief Converts an `uint32_t` into its digit characters (base 10) and copies them to the \p + * destination #az_span starting at its 0-th index. + * + * @param destination The #az_span where the bytes should be copied to. + * @param[in] source The `uint32_t` whose number is copied to the \p destination #az_span as ASCII + * digits. + * @param[out] out_span A pointer to an #az_span that receives the remainder of the \p destination + * #az_span after the `uint32_t` has been copied. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The \p destination is not big enough to contain the copied + * bytes. + */ +AZ_NODISCARD az_result az_span_u32toa(az_span destination, uint32_t source, az_span* out_span); + +/** + * @brief Converts an `int64_t` into its digit characters (base 10) and copies them to the \p + * destination #az_span starting at its 0-th index. + * + * @param destination The #az_span where the bytes should be copied to. + * @param[in] source The `int64_t` whose number is copied to the \p destination #az_span as ASCII + * digits. + * @param[out] out_span A pointer to an #az_span that receives the remainder of the \p destination + * #az_span after the `int64_t` has been copied. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The \p destination is not big enough to contain the copied + * bytes. + */ +AZ_NODISCARD az_result az_span_i64toa(az_span destination, int64_t source, az_span* out_span); + +/** + * @brief Converts a `uint64_t` into its digit characters (base 10) and copies them to the \p + * destination #az_span starting at its 0-th index. + * + * @param destination The #az_span where the bytes should be copied to. + * @param[in] source The `uint64_t` whose number is copied to the \p destination #az_span as ASCII + * digits. + * @param[out] out_span A pointer to an #az_span that receives the remainder of the \p destination + * #az_span after the `uint64_t` has been copied. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The \p destination is not big enough to contain the copied + * bytes. + */ +AZ_NODISCARD az_result az_span_u64toa(az_span destination, uint64_t source, az_span* out_span); + +/** + * @brief Converts a `double` into its digit characters (base 10 decimal notation) and copies them + * to the \p destination #az_span starting at its 0-th index. + * + * @param destination The #az_span where the bytes should be copied to. + * @param[in] source The `double` whose number is copied to the \p destination #az_span as ASCII + * digits and characters. + * @param[in] fractional_digits The number of digits to write into the \p destination #az_span after + * the decimal point and truncate the rest. + * @param[out] out_span A pointer to an #az_span that receives the remainder of the \p destination + * #az_span after the `double` has been copied. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval #AZ_ERROR_NOT_ENOUGH_SPACE The \p destination is not big enough to contain the copied + * bytes. + * @retval #AZ_ERROR_NOT_SUPPORTED The \p source is not a finite decimal number or contains an + * integer component that is too large and would overflow beyond `2^53 - 1`. + * + * @remark Only finite `double` values are supported. Values such as `NaN` and `INFINITY` are not + * allowed. + * + * @remark Non-significant trailing zeros (after the decimal point) are not written, even if \p + * fractional_digits is large enough to allow the zero padding. + * + * @remark The \p fractional_digits must be between 0 and 15 (inclusive). Any value passed in that + * is larger will be clamped down to 15. + */ +AZ_NODISCARD az_result +az_span_dtoa(az_span destination, double source, int32_t fractional_digits, az_span* out_span); + +/****************************** NON-CONTIGUOUS SPAN */ + +/** + * @brief Defines a container of required and user-defined fields that provide the + * necessary information and parameters for the implementation of the #az_span_allocator_fn + * callback. + */ +typedef struct +{ + /// Any struct that was provided by the user for their specific implementation, passed through to + /// the #az_span_allocator_fn. + void* user_context; + + /// The amount of space consumed (i.e. written into) within the previously provided destination, + /// which can be used to infer the remaining number of bytes of the #az_span that are leftover. + int32_t bytes_used; + + /// The minimum length of the destination #az_span required to be provided by the callback. If 0, + /// any non-empty sized buffer must be returned. + int32_t minimum_required_size; +} az_span_allocator_context; + +/** + * @brief Defines the signature of the callback function that the caller must implement to provide + * the potentially discontiguous destination buffers where output can be written into. + * + * @param[in] allocator_context A container of required and user-defined fields that provide the + * necessary information and parameters for the implementation of the callback. + * @param[out] out_next_destination A pointer to an #az_span that can be used as a destination to + * write data into, that is at least the required size specified within the \p allocator_context. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval other Failure. + * + * @remarks The caller must no longer hold onto, use, or write to the previously provided #az_span + * after this allocator returns a new destination #az_span. + * + * @remarks There is no guarantee that successive calls will return the same or same-sized buffer. + * This function must never return an empty #az_span, unless the requested buffer size is not + * available. In which case, it must return an error #az_result. + * + * @remarks The caller must check the return value using #az_result_failed() before continuing to + * use the \p out_next_destination. + */ +typedef az_result (*az_span_allocator_fn)( + az_span_allocator_context* allocator_context, + az_span* out_next_destination); + +#include <_az_cfg_suffix.h> + +#endif // _az_SPAN_H diff --git a/src/az_span_internal.h b/src/az_span_internal.h new file mode 100644 index 00000000..fb2029ef --- /dev/null +++ b/src/az_span_internal.h @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#ifndef _az_SPAN_INTERNAL_H +#define _az_SPAN_INTERNAL_H + +#include +#include +#include + +#include + +#include <_az_cfg_prefix.h> + +// The smallest number that has the same number of digits as _az_MAX_SIZE_FOR_UINT64 (i.e. 10^19). +#define _az_SMALLEST_20_DIGIT_NUMBER 10000000000000000000ULL + +enum +{ + // For example: 2,147,483,648 + _az_MAX_SIZE_FOR_UINT32 = 10, + + // For example: 18,446,744,073,709,551,615 + _az_MAX_SIZE_FOR_UINT64 = 20, + + // The number of unique values in base 10 (decimal). + _az_NUMBER_OF_DECIMAL_VALUES = 10, + + // The smallest number that has the same number of digits as _az_MAX_SIZE_FOR_UINT32 (i.e. 10^9). + _az_SMALLEST_10_DIGIT_NUMBER = 1000000000, +}; + +// Use this helper to figure out how much the sliced_span has moved in comparison to the +// original_span while writing and slicing a copy of the original. +// The \p sliced_span must be some slice of the \p original_span (and have the same backing memory). +AZ_INLINE AZ_NODISCARD int32_t _az_span_diff(az_span sliced_span, az_span original_span) +{ + int32_t answer = az_span_size(original_span) - az_span_size(sliced_span); + + // The passed in span parameters cannot be any two arbitrary spans. + // This validates the span parameters are valid and one is a sub-slice of another. + _az_PRECONDITION(answer == (int32_t)(az_span_ptr(sliced_span) - az_span_ptr(original_span))); + return answer; +} + +/** + * @brief Copies character from the \p source #az_span to the \p destination #az_span by + * URL-encoding the \p source span characters. + * + * @param destination The #az_span whose bytes will receive the URL-encoded \p source. + * @param[in] source The #az_span containing the non-URL-encoded bytes. + * @param[out] out_length A pointer to an int32_t that is going to be assigned the length + * of URL-encoding the \p source. + * @return An #az_result value indicating the result of the operation: + * - #AZ_OK if successful + * - #AZ_ERROR_NOT_ENOUGH_SPACE if the \p destination is not big enough to contain the + * encoded bytes + * + * @remark If \p destination can't fit the \p source, some data may still be written to it, but the + * \p out_length will be set to 0, and the function will return #AZ_ERROR_NOT_ENOUGH_SPACE. + * @remark The \p destination and \p source must not overlap. + */ +AZ_NODISCARD az_result +_az_span_url_encode(az_span destination, az_span source, int32_t* out_length); + +/** + * @brief Calculates what would be the length of \p source #az_span after url-encoding it. + * + * @param[in] source The #az_span containing the non-URL-encoded bytes. + * @return The length of source if it would be url-encoded. + * + */ +AZ_NODISCARD int32_t _az_span_url_encode_calc_length(az_span source); + +/** + * @brief String tokenizer for #az_span. + * + * @param[in] source The #az_span with the content to be searched on. It must be a non-empty + * #az_span. + * @param[in] delimiter The #az_span containing the delimiter to "split" `source` into tokens. It + * must be a non-empty #az_span. + * @param[out] out_remainder The #az_span pointing to the remaining bytes in `source`, starting + * after the occurrence of `delimiter`. If the position after `delimiter` is the end of `source`, + * `out_remainder` is set to an empty #az_span. + * @param[out] out_index The position of \p delimiter in \p source if \p source contains the \p + * delimiter within it. Otherwise, it is set to -1. + * + * @return The #az_span pointing to the token delimited by the beginning of `source` up to the first + * occurrence of (but not including the) `delimiter`, or the end of `source` if `delimiter` is not + * found. If `source` is empty, #AZ_SPAN_EMPTY is returned instead. + */ +az_span _az_span_token( + az_span source, + az_span delimiter, + az_span* out_remainder, + int32_t* out_index); + +#include <_az_cfg_suffix.h> + +#endif // _az_SPAN_INTERNAL_H diff --git a/src/az_span_private.h b/src/az_span_private.h new file mode 100644 index 00000000..523cad6b --- /dev/null +++ b/src/az_span_private.h @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#ifndef _az_SPAN_PRIVATE_H +#define _az_SPAN_PRIVATE_H + +#include +#include +#include +#include + +#include + +#include <_az_cfg_prefix.h> + +// In IEEE 754, +inf is represented as 0 for the sign bit, all 1s for the biased exponent, and 0s +// for the fraction bits. +#define _az_BINARY_VALUE_OF_POSITIVE_INFINITY 0x7FF0000000000000ULL + +enum +{ + _az_ASCII_LOWER_DIF = 'a' - 'A', + + // One less than the number of digits in _az_MAX_SAFE_INTEGER + // This many digits can roundtrip between double and uint64_t without loss of precision + // or causing integer overflow. We can't choose 16, because 9999999999999999 is larger than + // _az_MAX_SAFE_INTEGER. + _az_MAX_SUPPORTED_FRACTIONAL_DIGITS = 15, + + // 10 + sign (i.e. -2,147,483,648) + _az_MAX_SIZE_FOR_INT32 = 11, + + // 19 + sign (i.e. -9,223,372,036,854,775,808) + _az_MAX_SIZE_FOR_INT64 = 20, + + // Two digit length to create the "format" passed to sscanf. + _az_MAX_SIZE_FOR_PARSING_DOUBLE = 99, + + // The number value of the ASCII space character ' '. + _az_ASCII_SPACE_CHARACTER = 0x20, + + // The largest digit in base 16 (hexadecimal). + _az_LARGEST_HEX_VALUE = 0xF, +}; + +/** + * @brief A portable implementation of the standard `isfinite()` function, which may not be + * available on certain embedded systems that use older compilers. + * + * @param value The 64-bit floating point value to test. + * @return `true` if the \p value is finite (that is, it is not infinite or not a number), otherwise + * return `false`. + */ +AZ_NODISCARD AZ_INLINE bool _az_isfinite(double value) +{ + uint64_t binary_value = 0; + + // Workaround for strict-aliasing rules. + // Get the 8-byte binary representation of the double value, by re-interpreting it as an uint64_t. + // NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) + memcpy(&binary_value, &value, sizeof(binary_value)); + + // These are the binary representations of the various non-finite value ranges, + // according to the IEEE 754 standard: + // +inf - 0x7FF0000000000000 + // -inf - 0xFFF0000000000000 Anything in the following + // nan - 0x7FF0000000000001 to 0x7FFFFFFFFFFFFFFF and 0xFFF0000000000001 to 0xFFFFFFFFFFFFFFFF + + // This is equivalent to checking the following ranges, condensed into a single check: + // (binary_value < 0x7FF0000000000000 || + // (binary_value > 0x7FFFFFFFFFFFFFFF && binary_value < 0xFFF0000000000000)) + return (binary_value & _az_BINARY_VALUE_OF_POSITIVE_INFINITY) + != _az_BINARY_VALUE_OF_POSITIVE_INFINITY; +} + +AZ_NODISCARD az_result _az_is_expected_span(az_span* ref_span, az_span expected); + +/** + * @brief Removes all leading and trailing whitespace characters from the \p span. Function will + * create a new #az_span pointing to the first non-whitespace (` `, \\n, \\r, \\t) character found + * in \p span and up to the last non-whitespace character. + * + * @remarks If \p span is full of non-whitespace characters, this function will return empty + * #az_span. + * + * Example: + * \code{.c} + * az_span a = AZ_SPAN_FROM_STR(" text with \\n spaces "); + * az_span b = _az_span_trim_whitespace(a); + * // assert( b == AZ_SPAN_FROM_STR("text with \\n spaces")); + * \endcode + * + * @param[in] source #az_span pointing to a memory address that might contain whitespace characters. + * @return The trimmed #az_span. + */ +AZ_NODISCARD az_span _az_span_trim_whitespace(az_span source); + +/** + * @brief Removes all leading whitespace characters from the start of \p span. + * Function will create a new #az_span pointing to the first non-whitespace (` `, \\n, \\r, \\t) + * character found in \p span and up to the last character. + * + * @remarks If \p span is full of non-whitespace characters, this function will return empty + * #az_span. + * + * Example: + * \code{.c} + * az_span a = AZ_SPAN_FROM_STR(" text with \\n spaces "); + * az_span b = _az_span_trim_whitespace_from_start(a); + * // assert( b == AZ_SPAN_FROM_STR("text with \\n spaces ")); + * \endcode + * + * @param[in] source #az_span pointing to a memory address that might contain whitespace characters. + * @return The trimmed #az_span. + */ +AZ_NODISCARD az_span _az_span_trim_whitespace_from_start(az_span source); + +/** + * @brief Removes all trailing whitespace characters from the end of \p span. + * Function will create a new #az_span pointing to the first character in \p span and up to the last + * non-whitespace (` `, \\n, \\r, \\t) character. + * + * @remarks If \p span is full of non-whitespace characters, this function will return empty + * #az_span. + * + * Example: + * \code{.c} + * az_span a = AZ_SPAN_FROM_STR(" text with \\n spaces "); + * az_span b = _az_span_trim_whitespace_from_end(a); + * // assert( b == AZ_SPAN_FROM_STR(" text with \\n spaces")); + * \endcode + * + * @param[in] source #az_span pointing to a memory address that might contain whitespace characters. + * @return The trimmed #az_span. + */ +AZ_NODISCARD az_span _az_span_trim_whitespace_from_end(az_span source); + +#include <_az_cfg_suffix.h> + +#endif // _az_SPAN_PRIVATE_H diff --git a/src/az_storage_blobs.h b/src/az_storage_blobs.h new file mode 100644 index 00000000..135c7de8 --- /dev/null +++ b/src/az_storage_blobs.h @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Definition for the Azure Blob Storage blob client. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_STORAGE_BLOBS_H +#define _az_STORAGE_BLOBS_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include <_az_cfg_prefix.h> + +/** + * @brief Allows customization of the blob client. + */ +typedef struct +{ + struct + { + /// Telemetry options. + _az_http_policy_telemetry_options telemetry_options; + + /// Service API version. + _az_http_policy_apiversion_options api_version; + } _internal; + + /// Optional values used to override the default retry policy options. + az_http_policy_retry_options retry_options; +} az_storage_blobs_blob_client_options; + +/** + * @brief Gets the default blob storage options. + * + * @details Call this to obtain an initialized #az_storage_blobs_blob_client_options structure that + * can be modified and passed to #az_storage_blobs_blob_client_init(). + * + * @remark Use this, for instance, when only caring about setting one option by calling this + * function and then overriding that specific option. + */ +AZ_NODISCARD az_storage_blobs_blob_client_options az_storage_blobs_blob_client_options_default(); + +/** + * @brief Azure Blob Storage Blob Client. + */ +typedef struct +{ + struct + { + _az_credential* credential; + _az_http_pipeline pipeline; + az_span blob_url; + az_span host; + az_storage_blobs_blob_client_options options; + uint8_t blob_url_buffer[AZ_HTTP_REQUEST_URL_BUFFER_SIZE]; + } _internal; +} az_storage_blobs_blob_client; + +/** + * @brief Initialize a client with default options. + * + * @param[out] out_client The blob client instance to initialize. + * @param[in] blob_url A blob URL. Must be a vaild URL, cannot be empty. + * @param credential The object used for authentication. #AZ_CREDENTIAL_ANONYMOUS should be + * used for SAS. + * @param[in] options __[nullable]__ A reference to an #az_storage_blobs_blob_client_options + * structure which defines custom behavior of the client. If `NULL` is passed, the client + * will use the default options (i.e. #az_storage_blobs_blob_client_options_default()). + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval other Failure. + */ +AZ_NODISCARD az_result az_storage_blobs_blob_client_init( + az_storage_blobs_blob_client* out_client, + az_span blob_url, + void* credential, + az_storage_blobs_blob_client_options const* options); + +/** + * @brief Allows customization of the upload operation. + */ +typedef struct +{ + struct + { + bool unused; + } _internal; +} az_storage_blobs_blob_upload_options; + +/** + * @brief Allows customization of the download operation. + */ +typedef struct +{ + struct + { + bool unused; + } _internal; +} az_storage_blobs_blob_download_options; + +/** + * @brief Gets the default blob upload options. + * + * @details Call this to obtain an initialized #az_storage_blobs_blob_upload_options structure. + * + * @remark Use this, for instance, when only caring about setting one option by calling this + * function and then overriding that specific option. + */ +AZ_NODISCARD AZ_INLINE az_storage_blobs_blob_upload_options +az_storage_blobs_blob_upload_options_default() +{ + return (az_storage_blobs_blob_upload_options){ 0 }; +} + +/** + * @brief Gets the default blob download options. + * + * @details Call this to obtain an initialized #az_storage_blobs_blob_download_options structure. + * + * @remark Use this, for instance, when only caring about setting one option by calling this + * function and then overriding that specific option. + */ +AZ_NODISCARD AZ_INLINE az_storage_blobs_blob_download_options +az_storage_blobs_blob_download_options_default() +{ + return (az_storage_blobs_blob_download_options){ 0 }; +} + +/** + * @brief Uploads a span contents to blob storage. + * + * @param[in] client An #az_storage_blobs_blob_client structure. + * @param[in] context __[nullable]__ A context to control the request lifetime. If `NULL` is passed, + * #az_context_application is used. + * @param[in] content The blob content to upload. + * @param[in] options __[nullable]__ A reference to an #az_storage_blobs_blob_upload_options + * structure which defines custom behavior for uploading the blob. If `NULL` is passed, the client + * will use the default options (i.e. #az_storage_blobs_blob_upload_options_default()). + * @param[in,out] ref_response An initialized #az_http_response where to write HTTP response into. + * See https://docs.microsoft.com/rest/api/storageservices/put-blob#response + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval other Failure. + */ +AZ_NODISCARD az_result az_storage_blobs_blob_upload( + az_storage_blobs_blob_client* client, + az_context* context, + az_span content, + az_storage_blobs_blob_upload_options const* options, + az_http_response* ref_response); + +/** + * @brief Downloads the blob. + * + * @param[in] client An #az_storage_blobs_blob_client structure. + * @param[in] context __[nullable]__ A context to control the request lifetime. If `NULL` is passed, + * #az_context_application is used. + * @param[in] options __[nullable]__ A reference to an #az_storage_blobs_blob_download_options + * structure which defines custom behavior for downloading the blob. If `NULL` is passed, the client + * will use the default options (i.e. #az_storage_blobs_blob_download_options_default()). + * @param[in,out] ref_response An initialized #az_http_response where to write HTTP response into. + * See https://docs.microsoft.com/rest/api/storageservices/get-blob#response. + * + * @return An #az_result value indicating the result of the operation. + * @retval #AZ_OK Success. + * @retval other Failure. + */ +AZ_NODISCARD az_result az_storage_blobs_blob_download( + az_storage_blobs_blob_client* client, + az_context* context, + az_storage_blobs_blob_download_options const* options, + az_http_response* ref_response); + +#include <_az_cfg_suffix.h> + +#endif // _az_STORAGE_BLOBS_H diff --git a/src/az_storage_blobs_blob_client.c b/src/az_storage_blobs_blob_client.c new file mode 100644 index 00000000..e1b1938b --- /dev/null +++ b/src/az_storage_blobs_blob_client.c @@ -0,0 +1,317 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include <_az_cfg.h> + +enum +{ + _az_STORAGE_HTTP_REQUEST_HEADER_BUFFER_SIZE = 10 * sizeof(_az_http_request_header), +}; + +AZ_NODISCARD az_storage_blobs_blob_client_options az_storage_blobs_blob_client_options_default() +{ + az_storage_blobs_blob_client_options options = (az_storage_blobs_blob_client_options){ 0 }; + options = (az_storage_blobs_blob_client_options) { + ._internal = { + .telemetry_options = _az_http_policy_telemetry_options_create(AZ_SPAN_FROM_STR("storage-blobs")), + .api_version = _az_http_policy_apiversion_options_default(), + }, + .retry_options = _az_http_policy_retry_options_default(), + }; + + options._internal.api_version._internal.name = AZ_SPAN_FROM_STR("x-ms-version"); + options._internal.api_version._internal.version = AZ_SPAN_FROM_STR("2019-02-02"); + options._internal.api_version._internal.option_location + = _az_http_policy_apiversion_option_location_header; + + // NOLINTNEXTLINE(readability-magic-numbers, cppcoreguidelines-avoid-magic-numbers) + options.retry_options.max_retries = 5; + + options.retry_options.retry_delay_msec = 1 * _az_TIME_MILLISECONDS_PER_SECOND; + + // NOLINTNEXTLINE(readability-magic-numbers, cppcoreguidelines-avoid-magic-numbers) + options.retry_options.max_retry_delay_msec = 30 * _az_TIME_MILLISECONDS_PER_SECOND; + + return options; +} + +AZ_INLINE az_span _az_get_host_from_url(az_span url) +{ + int32_t const url_length = az_span_size(url); + uint8_t const* url_ptr = az_span_ptr(url); + + if (url_length >= (int32_t)(sizeof("s://h") - 1)) + { + int32_t const colon_max = url_length - (int32_t)(sizeof("//h") - 1); + for (int32_t colon_pos = 0; colon_pos < colon_max; ++colon_pos) + { + if (url_ptr[colon_pos] == ':') + { + if (url_ptr[colon_pos + 1] == '/' && url_ptr[colon_pos + 2] == '/') + { + int32_t const authority_pos = colon_pos + 3; + int32_t authority_end_pos = authority_pos; + for (; authority_end_pos < url_length; ++authority_end_pos) + { + if (url_ptr[authority_end_pos] == '/') + { + break; + } + } + + if (authority_end_pos > authority_pos) + { + int32_t host_start_pos = authority_pos; + for (; host_start_pos < authority_end_pos; ++host_start_pos) + { + if (url_ptr[host_start_pos] == '@') + { + break; + } + } + + if (host_start_pos == authority_end_pos) + { + host_start_pos = authority_pos; + } + else + { + ++host_start_pos; + } + + if (host_start_pos < authority_end_pos) + { + int32_t host_end_pos = host_start_pos; + for (; host_end_pos < authority_end_pos; ++host_end_pos) + { + if (url_ptr[host_end_pos] == ':') + { + break; + } + } + + return az_span_slice(url, host_start_pos, host_end_pos); + } + } + } + + break; + } + } + } + + return AZ_SPAN_EMPTY; +} + +AZ_NODISCARD az_result az_storage_blobs_blob_client_init( + az_storage_blobs_blob_client* out_client, + az_span blob_url, + void* credential, + az_storage_blobs_blob_client_options const* options) +{ + _az_PRECONDITION_NOT_NULL(out_client); + _az_PRECONDITION_VALID_SPAN(blob_url, sizeof("s://h") - 1, false); + + _az_credential* const cred = (_az_credential*)credential; + + *out_client = (az_storage_blobs_blob_client){ 0 }; + *out_client = (az_storage_blobs_blob_client) { + ._internal = { + .credential = cred, + .pipeline = (_az_http_pipeline){ + ._internal = { + .policies = { + { + ._internal = { + .process = az_http_pipeline_policy_apiversion, + .options= &out_client->_internal.options._internal.api_version, + }, + }, + { + ._internal = { + .process = az_http_pipeline_policy_telemetry, + .options = &out_client->_internal.options._internal.telemetry_options, + }, + }, + { + ._internal = { + .process = az_http_pipeline_policy_retry, + .options = &out_client->_internal.options.retry_options, + }, + }, + { + ._internal = { + .process = az_http_pipeline_policy_credential, + .options = cred, + }, + }, +#ifndef AZ_NO_LOGGING + { + ._internal = { + .process = az_http_pipeline_policy_logging, + .options = NULL, + }, + }, +#endif // AZ_NO_LOGGING + { + ._internal = { + .process = az_http_pipeline_policy_transport, + .options = NULL, + }, + }, + }, + } + }, + .blob_url = AZ_SPAN_FROM_BUFFER(out_client->_internal.blob_url_buffer), + .host = AZ_SPAN_EMPTY, + .options = (options != NULL) ? *options : az_storage_blobs_blob_client_options_default(), + } + }; + + // Copy url to client buffer so customer can re-use buffer on his/her side + int32_t const blob_url_size = az_span_size(blob_url); + _az_RETURN_IF_NOT_ENOUGH_SIZE(out_client->_internal.blob_url, blob_url_size); + az_span_copy(out_client->_internal.blob_url, blob_url); + out_client->_internal.blob_url = az_span_slice(out_client->_internal.blob_url, 0, blob_url_size); + + out_client->_internal.host = _az_get_host_from_url(out_client->_internal.blob_url); + + _az_RETURN_IF_FAILED( + _az_credential_set_scopes(cred, AZ_SPAN_FROM_STR("https://storage.azure.com/.default"))); + + return AZ_OK; +} + +static AZ_NODISCARD az_result _az_init_blob_client_http_request( + az_http_request* out_request, + az_context* context, + az_storage_blobs_blob_client const* client, + az_span request_url_span, + az_span request_headers_span, + az_http_method http_method, + az_span body) +{ + // URL buffer + az_span const blob_url = client->_internal.blob_url; + int32_t const url_size = az_span_size(blob_url); + _az_RETURN_IF_NOT_ENOUGH_SIZE(request_url_span, url_size); + az_span_copy(request_url_span, blob_url); + + // Request + _az_RETURN_IF_FAILED(az_http_request_init( + out_request, + (context != NULL) ? context : &az_context_application, + http_method, + request_url_span, + url_size, + request_headers_span, + body)); + + // Host header + az_span const host_val = client->_internal.host; + if (az_span_size(host_val) > 0) + { + _az_RETURN_IF_FAILED( + az_http_request_append_header(out_request, AZ_SPAN_FROM_STR("Host"), host_val)); + } + + return AZ_OK; +} + +AZ_NODISCARD az_result az_storage_blobs_blob_upload( + az_storage_blobs_blob_client* client, + az_context* context, + az_span content, + az_storage_blobs_blob_upload_options const* options, + az_http_response* ref_response) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(ref_response); + + (void)options; + + // HTTP request buffers + uint8_t url_buffer[AZ_HTTP_REQUEST_URL_BUFFER_SIZE] = { 0 }; + uint8_t headers_buffer[_az_STORAGE_HTTP_REQUEST_HEADER_BUFFER_SIZE] = { 0 }; + + // Initialize the HTTP request + az_http_request request = { 0 }; + _az_RETURN_IF_FAILED(_az_init_blob_client_http_request( + &request, + context, + client, + AZ_SPAN_FROM_BUFFER(url_buffer), + AZ_SPAN_FROM_BUFFER(headers_buffer), + az_http_method_put(), + content)); + + // Blob Type header + _az_RETURN_IF_FAILED(az_http_request_append_header( + &request, AZ_SPAN_FROM_STR("x-ms-blob-type"), AZ_SPAN_FROM_STR("BlockBlob"))); + + // Content Length header + uint8_t content_length[_az_INT64_AS_STR_BUFFER_SIZE] = { 0 }; + { + // Form the value + az_span content_length_span = AZ_SPAN_FROM_BUFFER(content_length); + az_span remainder = { 0 }; + _az_RETURN_IF_FAILED(az_span_i64toa(content_length_span, az_span_size(content), &remainder)); + content_length_span + = az_span_slice(content_length_span, 0, _az_span_diff(remainder, content_length_span)); + + // Append the header + _az_RETURN_IF_FAILED(az_http_request_append_header( + &request, AZ_SPAN_FROM_STR("Content-Length"), content_length_span)); + } + + // Content Type header + _az_RETURN_IF_FAILED(az_http_request_append_header( + &request, AZ_SPAN_FROM_STR("Content-Type"), AZ_SPAN_FROM_STR("text/plain"))); + + // Run the pipeline + return az_http_pipeline_process(&client->_internal.pipeline, &request, ref_response); +} + +AZ_NODISCARD az_result az_storage_blobs_blob_download( + az_storage_blobs_blob_client* client, + az_context* context, + az_storage_blobs_blob_download_options const* options, + az_http_response* ref_response) +{ + _az_PRECONDITION_NOT_NULL(client); + _az_PRECONDITION_NOT_NULL(ref_response); + + (void)options; + + // HTTP request buffers + uint8_t url_buffer[AZ_HTTP_REQUEST_URL_BUFFER_SIZE] = { 0 }; + uint8_t headers_buffer[_az_STORAGE_HTTP_REQUEST_HEADER_BUFFER_SIZE] = { 0 }; + + // Initialize the HTTP request + az_http_request request = { 0 }; + _az_RETURN_IF_FAILED(_az_init_blob_client_http_request( + &request, + context, + client, + AZ_SPAN_FROM_BUFFER(url_buffer), + AZ_SPAN_FROM_BUFFER(headers_buffer), + az_http_method_get(), + AZ_SPAN_EMPTY)); + + // Run the pipeline + return az_http_pipeline_process(&client->_internal.pipeline, &request, ref_response); +} diff --git a/src/az_version.h b/src/az_version.h new file mode 100644 index 00000000..253640a2 --- /dev/null +++ b/src/az_version.h @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +/** + * @file + * + * @brief Provides version information. + * + * @note You MUST NOT use any symbols (macros, functions, structures, enums, etc.) + * prefixed with an underscore ('_') directly in your application code. These symbols + * are part of Azure SDK's internal implementation; we do not document these symbols + * and they are subject to change in future versions of the SDK which would break your code. + */ + +#ifndef _az_VERSION_H +#define _az_VERSION_H + +/// The version in string format used for telemetry following the `semver.org` standard +/// (https://semver.org). +#define AZ_SDK_VERSION_STRING "1.3.0-beta.1" + +/// Major numeric identifier. +#define AZ_SDK_VERSION_MAJOR 1 + +/// Minor numeric identifier. +#define AZ_SDK_VERSION_MINOR 3 + +/// Patch numeric identifier. +#define AZ_SDK_VERSION_PATCH 0 + +/// Optional pre-release identifier. SDK is in a pre-release state when present. +#define AZ_SDK_VERSION_PRERELEASE "beta.1" + +#endif //_az_VERSION_H diff --git a/src/azure_ca.h b/src/azure_ca.h new file mode 100644 index 00000000..6e65d145 --- /dev/null +++ b/src/azure_ca.h @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// SPDX-License-Identifier: MIT + +unsigned char ca_pem[] = { + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, 0x20, 0x43, + 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x44, 0x64, 0x7a, 0x43, 0x43, + 0x41, 0x6c, 0x2b, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, 0x49, 0x45, + 0x41, 0x67, 0x41, 0x41, 0x75, 0x54, 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, + 0x68, 0x6b, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x55, 0x46, + 0x41, 0x44, 0x42, 0x61, 0x4d, 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, + 0x56, 0x51, 0x51, 0x47, 0x45, 0x77, 0x4a, 0x4a, 0x0a, 0x52, 0x54, 0x45, + 0x53, 0x4d, 0x42, 0x41, 0x47, 0x41, 0x31, 0x55, 0x45, 0x43, 0x68, 0x4d, + 0x4a, 0x51, 0x6d, 0x46, 0x73, 0x64, 0x47, 0x6c, 0x74, 0x62, 0x33, 0x4a, + 0x6c, 0x4d, 0x52, 0x4d, 0x77, 0x45, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, + 0x4c, 0x45, 0x77, 0x70, 0x44, 0x65, 0x57, 0x4a, 0x6c, 0x63, 0x6c, 0x52, + 0x79, 0x64, 0x58, 0x4e, 0x30, 0x4d, 0x53, 0x49, 0x77, 0x49, 0x41, 0x59, + 0x44, 0x0a, 0x56, 0x51, 0x51, 0x44, 0x45, 0x78, 0x6c, 0x43, 0x59, 0x57, + 0x78, 0x30, 0x61, 0x57, 0x31, 0x76, 0x63, 0x6d, 0x55, 0x67, 0x51, 0x33, + 0x6c, 0x69, 0x5a, 0x58, 0x4a, 0x55, 0x63, 0x6e, 0x56, 0x7a, 0x64, 0x43, + 0x42, 0x53, 0x62, 0x32, 0x39, 0x30, 0x4d, 0x42, 0x34, 0x58, 0x44, 0x54, + 0x41, 0x77, 0x4d, 0x44, 0x55, 0x78, 0x4d, 0x6a, 0x45, 0x34, 0x4e, 0x44, + 0x59, 0x77, 0x4d, 0x46, 0x6f, 0x58, 0x0a, 0x44, 0x54, 0x49, 0x31, 0x4d, + 0x44, 0x55, 0x78, 0x4d, 0x6a, 0x49, 0x7a, 0x4e, 0x54, 0x6b, 0x77, 0x4d, + 0x46, 0x6f, 0x77, 0x57, 0x6a, 0x45, 0x4c, 0x4d, 0x41, 0x6b, 0x47, 0x41, + 0x31, 0x55, 0x45, 0x42, 0x68, 0x4d, 0x43, 0x53, 0x55, 0x55, 0x78, 0x45, + 0x6a, 0x41, 0x51, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x6f, 0x54, 0x43, + 0x55, 0x4a, 0x68, 0x62, 0x48, 0x52, 0x70, 0x62, 0x57, 0x39, 0x79, 0x0a, + 0x5a, 0x54, 0x45, 0x54, 0x4d, 0x42, 0x45, 0x47, 0x41, 0x31, 0x55, 0x45, + 0x43, 0x78, 0x4d, 0x4b, 0x51, 0x33, 0x6c, 0x69, 0x5a, 0x58, 0x4a, 0x55, + 0x63, 0x6e, 0x56, 0x7a, 0x64, 0x44, 0x45, 0x69, 0x4d, 0x43, 0x41, 0x47, + 0x41, 0x31, 0x55, 0x45, 0x41, 0x78, 0x4d, 0x5a, 0x51, 0x6d, 0x46, 0x73, + 0x64, 0x47, 0x6c, 0x74, 0x62, 0x33, 0x4a, 0x6c, 0x49, 0x45, 0x4e, 0x35, + 0x59, 0x6d, 0x56, 0x79, 0x0a, 0x56, 0x48, 0x4a, 0x31, 0x63, 0x33, 0x51, + 0x67, 0x55, 0x6d, 0x39, 0x76, 0x64, 0x44, 0x43, 0x43, 0x41, 0x53, 0x49, + 0x77, 0x44, 0x51, 0x59, 0x4a, 0x4b, 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x63, + 0x4e, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, + 0x50, 0x41, 0x44, 0x43, 0x43, 0x41, 0x51, 0x6f, 0x43, 0x67, 0x67, 0x45, + 0x42, 0x41, 0x4b, 0x4d, 0x45, 0x75, 0x79, 0x4b, 0x72, 0x0a, 0x6d, 0x44, + 0x31, 0x58, 0x36, 0x43, 0x5a, 0x79, 0x6d, 0x72, 0x56, 0x35, 0x31, 0x43, + 0x6e, 0x69, 0x34, 0x65, 0x69, 0x56, 0x67, 0x4c, 0x47, 0x77, 0x34, 0x31, + 0x75, 0x4f, 0x4b, 0x79, 0x6d, 0x61, 0x5a, 0x4e, 0x2b, 0x68, 0x58, 0x65, + 0x32, 0x77, 0x43, 0x51, 0x56, 0x74, 0x32, 0x79, 0x67, 0x75, 0x7a, 0x6d, + 0x4b, 0x69, 0x59, 0x76, 0x36, 0x30, 0x69, 0x4e, 0x6f, 0x53, 0x36, 0x7a, + 0x6a, 0x72, 0x0a, 0x49, 0x5a, 0x33, 0x41, 0x51, 0x53, 0x73, 0x42, 0x55, + 0x6e, 0x75, 0x49, 0x64, 0x39, 0x4d, 0x63, 0x6a, 0x38, 0x65, 0x36, 0x75, + 0x59, 0x69, 0x31, 0x61, 0x67, 0x6e, 0x6e, 0x63, 0x2b, 0x67, 0x52, 0x51, + 0x4b, 0x66, 0x52, 0x7a, 0x4d, 0x70, 0x69, 0x6a, 0x53, 0x33, 0x6c, 0x6a, + 0x77, 0x75, 0x6d, 0x55, 0x4e, 0x4b, 0x6f, 0x55, 0x4d, 0x4d, 0x6f, 0x36, + 0x76, 0x57, 0x72, 0x4a, 0x59, 0x65, 0x4b, 0x0a, 0x6d, 0x70, 0x59, 0x63, + 0x71, 0x57, 0x65, 0x34, 0x50, 0x77, 0x7a, 0x56, 0x39, 0x2f, 0x6c, 0x53, + 0x45, 0x79, 0x2f, 0x43, 0x47, 0x39, 0x56, 0x77, 0x63, 0x50, 0x43, 0x50, + 0x77, 0x42, 0x4c, 0x4b, 0x42, 0x73, 0x75, 0x61, 0x34, 0x64, 0x6e, 0x4b, + 0x4d, 0x33, 0x70, 0x33, 0x31, 0x76, 0x6a, 0x73, 0x75, 0x66, 0x46, 0x6f, + 0x52, 0x45, 0x4a, 0x49, 0x45, 0x39, 0x4c, 0x41, 0x77, 0x71, 0x53, 0x75, + 0x0a, 0x58, 0x6d, 0x44, 0x2b, 0x74, 0x71, 0x59, 0x46, 0x2f, 0x4c, 0x54, + 0x64, 0x42, 0x31, 0x6b, 0x43, 0x31, 0x46, 0x6b, 0x59, 0x6d, 0x47, 0x50, + 0x31, 0x70, 0x57, 0x50, 0x67, 0x6b, 0x41, 0x78, 0x39, 0x58, 0x62, 0x49, + 0x47, 0x65, 0x76, 0x4f, 0x46, 0x36, 0x75, 0x76, 0x55, 0x41, 0x36, 0x35, + 0x65, 0x68, 0x44, 0x35, 0x66, 0x2f, 0x78, 0x58, 0x74, 0x61, 0x62, 0x7a, + 0x35, 0x4f, 0x54, 0x5a, 0x79, 0x0a, 0x64, 0x63, 0x39, 0x33, 0x55, 0x6b, + 0x33, 0x7a, 0x79, 0x5a, 0x41, 0x73, 0x75, 0x54, 0x33, 0x6c, 0x79, 0x53, + 0x4e, 0x54, 0x50, 0x78, 0x38, 0x6b, 0x6d, 0x43, 0x46, 0x63, 0x42, 0x35, + 0x6b, 0x70, 0x76, 0x63, 0x59, 0x36, 0x37, 0x4f, 0x64, 0x75, 0x68, 0x6a, + 0x70, 0x72, 0x6c, 0x33, 0x52, 0x6a, 0x4d, 0x37, 0x31, 0x6f, 0x47, 0x44, + 0x48, 0x77, 0x65, 0x49, 0x31, 0x32, 0x76, 0x2f, 0x79, 0x65, 0x0a, 0x6a, + 0x6c, 0x30, 0x71, 0x68, 0x71, 0x64, 0x4e, 0x6b, 0x4e, 0x77, 0x6e, 0x47, + 0x6a, 0x6b, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x61, 0x4e, 0x46, 0x4d, + 0x45, 0x4d, 0x77, 0x48, 0x51, 0x59, 0x44, 0x56, 0x52, 0x30, 0x4f, 0x42, + 0x42, 0x59, 0x45, 0x46, 0x4f, 0x57, 0x64, 0x57, 0x54, 0x43, 0x43, 0x52, + 0x31, 0x6a, 0x4d, 0x72, 0x50, 0x6f, 0x49, 0x56, 0x44, 0x61, 0x47, 0x65, + 0x7a, 0x71, 0x31, 0x0a, 0x42, 0x45, 0x33, 0x77, 0x4d, 0x42, 0x49, 0x47, + 0x41, 0x31, 0x55, 0x64, 0x45, 0x77, 0x45, 0x42, 0x2f, 0x77, 0x51, 0x49, + 0x4d, 0x41, 0x59, 0x42, 0x41, 0x66, 0x38, 0x43, 0x41, 0x51, 0x4d, 0x77, + 0x44, 0x67, 0x59, 0x44, 0x56, 0x52, 0x30, 0x50, 0x41, 0x51, 0x48, 0x2f, + 0x42, 0x41, 0x51, 0x44, 0x41, 0x67, 0x45, 0x47, 0x4d, 0x41, 0x30, 0x47, + 0x43, 0x53, 0x71, 0x47, 0x53, 0x49, 0x62, 0x33, 0x0a, 0x44, 0x51, 0x45, + 0x42, 0x42, 0x51, 0x55, 0x41, 0x41, 0x34, 0x49, 0x42, 0x41, 0x51, 0x43, + 0x46, 0x44, 0x46, 0x32, 0x4f, 0x35, 0x47, 0x39, 0x52, 0x61, 0x45, 0x49, + 0x46, 0x6f, 0x4e, 0x32, 0x37, 0x54, 0x79, 0x63, 0x6c, 0x68, 0x41, 0x4f, + 0x39, 0x39, 0x32, 0x54, 0x39, 0x4c, 0x64, 0x63, 0x77, 0x34, 0x36, 0x51, + 0x51, 0x46, 0x2b, 0x76, 0x61, 0x4b, 0x53, 0x6d, 0x32, 0x65, 0x54, 0x39, + 0x32, 0x0a, 0x39, 0x68, 0x6b, 0x54, 0x49, 0x37, 0x67, 0x51, 0x43, 0x76, + 0x6c, 0x59, 0x70, 0x4e, 0x52, 0x68, 0x63, 0x4c, 0x30, 0x45, 0x59, 0x57, + 0x6f, 0x53, 0x69, 0x68, 0x66, 0x56, 0x43, 0x72, 0x33, 0x46, 0x76, 0x44, + 0x42, 0x38, 0x31, 0x75, 0x6b, 0x4d, 0x4a, 0x59, 0x32, 0x47, 0x51, 0x45, + 0x2f, 0x73, 0x7a, 0x4b, 0x4e, 0x2b, 0x4f, 0x4d, 0x59, 0x33, 0x45, 0x55, + 0x2f, 0x74, 0x33, 0x57, 0x67, 0x78, 0x0a, 0x6a, 0x6b, 0x7a, 0x53, 0x73, + 0x77, 0x46, 0x30, 0x37, 0x72, 0x35, 0x31, 0x58, 0x67, 0x64, 0x49, 0x47, + 0x6e, 0x39, 0x77, 0x2f, 0x78, 0x5a, 0x63, 0x68, 0x4d, 0x42, 0x35, 0x68, + 0x62, 0x67, 0x46, 0x2f, 0x58, 0x2b, 0x2b, 0x5a, 0x52, 0x47, 0x6a, 0x44, + 0x38, 0x41, 0x43, 0x74, 0x50, 0x68, 0x53, 0x4e, 0x7a, 0x6b, 0x45, 0x31, + 0x61, 0x6b, 0x78, 0x65, 0x68, 0x69, 0x2f, 0x6f, 0x43, 0x72, 0x30, 0x0a, + 0x45, 0x70, 0x6e, 0x33, 0x6f, 0x30, 0x57, 0x43, 0x34, 0x7a, 0x78, 0x65, + 0x39, 0x5a, 0x32, 0x65, 0x74, 0x63, 0x69, 0x65, 0x66, 0x43, 0x37, 0x49, + 0x70, 0x4a, 0x35, 0x4f, 0x43, 0x42, 0x52, 0x4c, 0x62, 0x66, 0x31, 0x77, + 0x62, 0x57, 0x73, 0x61, 0x59, 0x37, 0x31, 0x6b, 0x35, 0x68, 0x2b, 0x33, + 0x7a, 0x76, 0x44, 0x79, 0x6e, 0x79, 0x36, 0x37, 0x47, 0x37, 0x66, 0x79, + 0x55, 0x49, 0x68, 0x7a, 0x0a, 0x6b, 0x73, 0x4c, 0x69, 0x34, 0x78, 0x61, + 0x4e, 0x6d, 0x6a, 0x49, 0x43, 0x71, 0x34, 0x34, 0x59, 0x33, 0x65, 0x6b, + 0x51, 0x45, 0x65, 0x35, 0x2b, 0x4e, 0x61, 0x75, 0x51, 0x72, 0x7a, 0x34, + 0x77, 0x6c, 0x48, 0x72, 0x51, 0x4d, 0x7a, 0x32, 0x6e, 0x5a, 0x51, 0x2f, + 0x31, 0x2f, 0x49, 0x36, 0x65, 0x59, 0x73, 0x39, 0x48, 0x52, 0x43, 0x77, + 0x42, 0x58, 0x62, 0x73, 0x64, 0x74, 0x54, 0x4c, 0x53, 0x0a, 0x52, 0x39, + 0x49, 0x34, 0x4c, 0x74, 0x44, 0x2b, 0x67, 0x64, 0x77, 0x79, 0x61, 0x68, + 0x36, 0x31, 0x37, 0x6a, 0x7a, 0x56, 0x2f, 0x4f, 0x65, 0x42, 0x48, 0x52, + 0x6e, 0x44, 0x4a, 0x45, 0x4c, 0x71, 0x59, 0x7a, 0x6d, 0x70, 0x0a, 0x2d, + 0x2d, 0x2d, 0x2d, 0x2d, 0x45, 0x4e, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, + 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, + 0x0a, 0x0a, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, + 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x44, 0x6a, 0x6a, + 0x43, 0x43, 0x41, 0x6e, 0x61, 0x67, 0x41, 0x77, 0x49, 0x42, 0x41, 0x67, + 0x49, 0x51, 0x41, 0x7a, 0x72, 0x78, 0x35, 0x71, 0x63, 0x52, 0x71, 0x61, + 0x43, 0x37, 0x4b, 0x47, 0x53, 0x78, 0x48, 0x51, 0x6e, 0x36, 0x35, 0x54, + 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, 0x68, 0x6b, 0x69, 0x47, 0x39, 0x77, + 0x30, 0x42, 0x41, 0x51, 0x73, 0x46, 0x41, 0x44, 0x42, 0x68, 0x0a, 0x4d, + 0x51, 0x73, 0x77, 0x43, 0x51, 0x59, 0x44, 0x56, 0x51, 0x51, 0x47, 0x45, + 0x77, 0x4a, 0x56, 0x55, 0x7a, 0x45, 0x56, 0x4d, 0x42, 0x4d, 0x47, 0x41, + 0x31, 0x55, 0x45, 0x43, 0x68, 0x4d, 0x4d, 0x52, 0x47, 0x6c, 0x6e, 0x61, + 0x55, 0x4e, 0x6c, 0x63, 0x6e, 0x51, 0x67, 0x53, 0x57, 0x35, 0x6a, 0x4d, + 0x52, 0x6b, 0x77, 0x46, 0x77, 0x59, 0x44, 0x56, 0x51, 0x51, 0x4c, 0x45, + 0x78, 0x42, 0x33, 0x0a, 0x64, 0x33, 0x63, 0x75, 0x5a, 0x47, 0x6c, 0x6e, + 0x61, 0x57, 0x4e, 0x6c, 0x63, 0x6e, 0x51, 0x75, 0x59, 0x32, 0x39, 0x74, + 0x4d, 0x53, 0x41, 0x77, 0x48, 0x67, 0x59, 0x44, 0x56, 0x51, 0x51, 0x44, + 0x45, 0x78, 0x64, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, 0x56, 0x79, + 0x64, 0x43, 0x42, 0x48, 0x62, 0x47, 0x39, 0x69, 0x59, 0x57, 0x77, 0x67, + 0x55, 0x6d, 0x39, 0x76, 0x64, 0x43, 0x42, 0x48, 0x0a, 0x4d, 0x6a, 0x41, + 0x65, 0x46, 0x77, 0x30, 0x78, 0x4d, 0x7a, 0x41, 0x34, 0x4d, 0x44, 0x45, + 0x78, 0x4d, 0x6a, 0x41, 0x77, 0x4d, 0x44, 0x42, 0x61, 0x46, 0x77, 0x30, + 0x7a, 0x4f, 0x44, 0x41, 0x78, 0x4d, 0x54, 0x55, 0x78, 0x4d, 0x6a, 0x41, + 0x77, 0x4d, 0x44, 0x42, 0x61, 0x4d, 0x47, 0x45, 0x78, 0x43, 0x7a, 0x41, + 0x4a, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x59, 0x54, 0x41, 0x6c, 0x56, + 0x54, 0x0a, 0x4d, 0x52, 0x55, 0x77, 0x45, 0x77, 0x59, 0x44, 0x56, 0x51, + 0x51, 0x4b, 0x45, 0x77, 0x78, 0x45, 0x61, 0x57, 0x64, 0x70, 0x51, 0x32, + 0x56, 0x79, 0x64, 0x43, 0x42, 0x4a, 0x62, 0x6d, 0x4d, 0x78, 0x47, 0x54, + 0x41, 0x58, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x73, 0x54, 0x45, 0x48, + 0x64, 0x33, 0x64, 0x79, 0x35, 0x6b, 0x61, 0x57, 0x64, 0x70, 0x59, 0x32, + 0x56, 0x79, 0x64, 0x43, 0x35, 0x6a, 0x0a, 0x62, 0x32, 0x30, 0x78, 0x49, + 0x44, 0x41, 0x65, 0x42, 0x67, 0x4e, 0x56, 0x42, 0x41, 0x4d, 0x54, 0x46, + 0x30, 0x52, 0x70, 0x5a, 0x32, 0x6c, 0x44, 0x5a, 0x58, 0x4a, 0x30, 0x49, + 0x45, 0x64, 0x73, 0x62, 0x32, 0x4a, 0x68, 0x62, 0x43, 0x42, 0x53, 0x62, + 0x32, 0x39, 0x30, 0x49, 0x45, 0x63, 0x79, 0x4d, 0x49, 0x49, 0x42, 0x49, + 0x6a, 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, 0x68, 0x6b, 0x69, 0x47, 0x0a, + 0x39, 0x77, 0x30, 0x42, 0x41, 0x51, 0x45, 0x46, 0x41, 0x41, 0x4f, 0x43, + 0x41, 0x51, 0x38, 0x41, 0x4d, 0x49, 0x49, 0x42, 0x43, 0x67, 0x4b, 0x43, + 0x41, 0x51, 0x45, 0x41, 0x75, 0x7a, 0x66, 0x4e, 0x4e, 0x4e, 0x78, 0x37, + 0x61, 0x38, 0x6d, 0x79, 0x61, 0x4a, 0x43, 0x74, 0x53, 0x6e, 0x58, 0x2f, + 0x52, 0x72, 0x6f, 0x68, 0x43, 0x67, 0x69, 0x4e, 0x39, 0x52, 0x6c, 0x55, + 0x79, 0x66, 0x75, 0x49, 0x0a, 0x32, 0x2f, 0x4f, 0x75, 0x38, 0x6a, 0x71, + 0x4a, 0x6b, 0x54, 0x78, 0x36, 0x35, 0x71, 0x73, 0x47, 0x47, 0x6d, 0x76, + 0x50, 0x72, 0x43, 0x33, 0x6f, 0x58, 0x67, 0x6b, 0x6b, 0x52, 0x4c, 0x70, + 0x69, 0x6d, 0x6e, 0x37, 0x57, 0x6f, 0x36, 0x68, 0x2b, 0x34, 0x46, 0x52, + 0x31, 0x49, 0x41, 0x57, 0x73, 0x55, 0x4c, 0x65, 0x63, 0x59, 0x78, 0x70, + 0x73, 0x4d, 0x4e, 0x7a, 0x61, 0x48, 0x78, 0x6d, 0x78, 0x0a, 0x31, 0x78, + 0x37, 0x65, 0x2f, 0x64, 0x66, 0x67, 0x79, 0x35, 0x53, 0x44, 0x4e, 0x36, + 0x37, 0x73, 0x48, 0x30, 0x4e, 0x4f, 0x33, 0x58, 0x73, 0x73, 0x30, 0x72, + 0x30, 0x75, 0x70, 0x53, 0x2f, 0x6b, 0x71, 0x62, 0x69, 0x74, 0x4f, 0x74, + 0x53, 0x5a, 0x70, 0x4c, 0x59, 0x6c, 0x36, 0x5a, 0x74, 0x72, 0x41, 0x47, + 0x43, 0x53, 0x59, 0x50, 0x39, 0x50, 0x49, 0x55, 0x6b, 0x59, 0x39, 0x32, + 0x65, 0x51, 0x0a, 0x71, 0x32, 0x45, 0x47, 0x6e, 0x49, 0x2f, 0x79, 0x75, + 0x75, 0x6d, 0x30, 0x36, 0x5a, 0x49, 0x79, 0x61, 0x37, 0x58, 0x7a, 0x56, + 0x2b, 0x68, 0x64, 0x47, 0x38, 0x32, 0x4d, 0x48, 0x61, 0x75, 0x56, 0x42, + 0x4a, 0x56, 0x4a, 0x38, 0x7a, 0x55, 0x74, 0x6c, 0x75, 0x4e, 0x4a, 0x62, + 0x64, 0x31, 0x33, 0x34, 0x2f, 0x74, 0x4a, 0x53, 0x37, 0x53, 0x73, 0x56, + 0x51, 0x65, 0x70, 0x6a, 0x35, 0x57, 0x7a, 0x0a, 0x74, 0x43, 0x4f, 0x37, + 0x54, 0x47, 0x31, 0x46, 0x38, 0x50, 0x61, 0x70, 0x73, 0x70, 0x55, 0x77, + 0x74, 0x50, 0x31, 0x4d, 0x56, 0x59, 0x77, 0x6e, 0x53, 0x6c, 0x63, 0x55, + 0x66, 0x49, 0x4b, 0x64, 0x7a, 0x58, 0x4f, 0x53, 0x30, 0x78, 0x5a, 0x4b, + 0x42, 0x67, 0x79, 0x4d, 0x55, 0x4e, 0x47, 0x50, 0x48, 0x67, 0x6d, 0x2b, + 0x46, 0x36, 0x48, 0x6d, 0x49, 0x63, 0x72, 0x39, 0x67, 0x2b, 0x55, 0x51, + 0x0a, 0x76, 0x49, 0x4f, 0x6c, 0x43, 0x73, 0x52, 0x6e, 0x4b, 0x50, 0x5a, + 0x7a, 0x46, 0x42, 0x51, 0x39, 0x52, 0x6e, 0x62, 0x44, 0x68, 0x78, 0x53, + 0x4a, 0x49, 0x54, 0x52, 0x4e, 0x72, 0x77, 0x39, 0x46, 0x44, 0x4b, 0x5a, + 0x4a, 0x6f, 0x62, 0x71, 0x37, 0x6e, 0x4d, 0x57, 0x78, 0x4d, 0x34, 0x4d, + 0x70, 0x68, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x6f, 0x30, 0x49, + 0x77, 0x51, 0x44, 0x41, 0x50, 0x0a, 0x42, 0x67, 0x4e, 0x56, 0x48, 0x52, + 0x4d, 0x42, 0x41, 0x66, 0x38, 0x45, 0x42, 0x54, 0x41, 0x44, 0x41, 0x51, + 0x48, 0x2f, 0x4d, 0x41, 0x34, 0x47, 0x41, 0x31, 0x55, 0x64, 0x44, 0x77, + 0x45, 0x42, 0x2f, 0x77, 0x51, 0x45, 0x41, 0x77, 0x49, 0x42, 0x68, 0x6a, + 0x41, 0x64, 0x42, 0x67, 0x4e, 0x56, 0x48, 0x51, 0x34, 0x45, 0x46, 0x67, + 0x51, 0x55, 0x54, 0x69, 0x4a, 0x55, 0x49, 0x42, 0x69, 0x56, 0x0a, 0x35, + 0x75, 0x4e, 0x75, 0x35, 0x67, 0x2f, 0x36, 0x2b, 0x72, 0x6b, 0x53, 0x37, + 0x51, 0x59, 0x58, 0x6a, 0x7a, 0x6b, 0x77, 0x44, 0x51, 0x59, 0x4a, 0x4b, + 0x6f, 0x5a, 0x49, 0x68, 0x76, 0x63, 0x4e, 0x41, 0x51, 0x45, 0x4c, 0x42, + 0x51, 0x41, 0x44, 0x67, 0x67, 0x45, 0x42, 0x41, 0x47, 0x42, 0x6e, 0x4b, + 0x4a, 0x52, 0x76, 0x44, 0x6b, 0x68, 0x6a, 0x36, 0x7a, 0x48, 0x64, 0x36, + 0x6d, 0x63, 0x59, 0x0a, 0x31, 0x59, 0x6c, 0x39, 0x50, 0x4d, 0x57, 0x4c, + 0x53, 0x6e, 0x2f, 0x70, 0x76, 0x74, 0x73, 0x72, 0x46, 0x39, 0x2b, 0x77, + 0x58, 0x33, 0x4e, 0x33, 0x4b, 0x6a, 0x49, 0x54, 0x4f, 0x59, 0x46, 0x6e, + 0x51, 0x6f, 0x51, 0x6a, 0x38, 0x6b, 0x56, 0x6e, 0x4e, 0x65, 0x79, 0x49, + 0x76, 0x2f, 0x69, 0x50, 0x73, 0x47, 0x45, 0x4d, 0x4e, 0x4b, 0x53, 0x75, + 0x49, 0x45, 0x79, 0x45, 0x78, 0x74, 0x76, 0x34, 0x0a, 0x4e, 0x65, 0x46, + 0x32, 0x32, 0x64, 0x2b, 0x6d, 0x51, 0x72, 0x76, 0x48, 0x52, 0x41, 0x69, + 0x47, 0x66, 0x7a, 0x5a, 0x30, 0x4a, 0x46, 0x72, 0x61, 0x62, 0x41, 0x30, + 0x55, 0x57, 0x54, 0x57, 0x39, 0x38, 0x6b, 0x6e, 0x64, 0x74, 0x68, 0x2f, + 0x4a, 0x73, 0x77, 0x31, 0x48, 0x4b, 0x6a, 0x32, 0x5a, 0x4c, 0x37, 0x74, + 0x63, 0x75, 0x37, 0x58, 0x55, 0x49, 0x4f, 0x47, 0x5a, 0x58, 0x31, 0x4e, + 0x47, 0x0a, 0x46, 0x64, 0x74, 0x6f, 0x6d, 0x2f, 0x44, 0x7a, 0x4d, 0x4e, + 0x55, 0x2b, 0x4d, 0x65, 0x4b, 0x4e, 0x68, 0x4a, 0x37, 0x6a, 0x69, 0x74, + 0x72, 0x61, 0x6c, 0x6a, 0x34, 0x31, 0x45, 0x36, 0x56, 0x66, 0x38, 0x50, + 0x6c, 0x77, 0x55, 0x48, 0x42, 0x48, 0x51, 0x52, 0x46, 0x58, 0x47, 0x55, + 0x37, 0x41, 0x6a, 0x36, 0x34, 0x47, 0x78, 0x4a, 0x55, 0x54, 0x46, 0x79, + 0x38, 0x62, 0x4a, 0x5a, 0x39, 0x31, 0x0a, 0x38, 0x72, 0x47, 0x4f, 0x6d, + 0x61, 0x46, 0x76, 0x45, 0x37, 0x46, 0x42, 0x63, 0x66, 0x36, 0x49, 0x4b, + 0x73, 0x68, 0x50, 0x45, 0x43, 0x42, 0x56, 0x31, 0x2f, 0x4d, 0x55, 0x52, + 0x65, 0x58, 0x67, 0x52, 0x50, 0x54, 0x71, 0x68, 0x35, 0x55, 0x79, 0x6b, + 0x77, 0x37, 0x2b, 0x55, 0x30, 0x62, 0x36, 0x4c, 0x4a, 0x33, 0x2f, 0x69, + 0x79, 0x4b, 0x35, 0x53, 0x39, 0x6b, 0x4a, 0x52, 0x61, 0x54, 0x65, 0x0a, + 0x70, 0x4c, 0x69, 0x61, 0x57, 0x4e, 0x30, 0x62, 0x66, 0x56, 0x4b, 0x66, + 0x6a, 0x6c, 0x6c, 0x44, 0x69, 0x49, 0x47, 0x6b, 0x6e, 0x69, 0x62, 0x56, + 0x62, 0x36, 0x33, 0x64, 0x44, 0x63, 0x59, 0x33, 0x66, 0x65, 0x30, 0x44, + 0x6b, 0x68, 0x76, 0x6c, 0x64, 0x31, 0x39, 0x32, 0x37, 0x6a, 0x79, 0x4e, + 0x78, 0x46, 0x31, 0x57, 0x57, 0x36, 0x4c, 0x5a, 0x5a, 0x6d, 0x36, 0x7a, + 0x4e, 0x54, 0x66, 0x6c, 0x0a, 0x4d, 0x72, 0x59, 0x3d, 0x0a, 0x2d, 0x2d, + 0x2d, 0x2d, 0x2d, 0x45, 0x4e, 0x44, 0x20, 0x43, 0x45, 0x52, 0x54, 0x49, + 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a, + 0x00 +}; +unsigned int ca_pem_len = 2557; diff --git a/tools/Update-Library.ps1 b/tools/Update-Library.ps1 new file mode 100644 index 00000000..a979b39d --- /dev/null +++ b/tools/Update-Library.ps1 @@ -0,0 +1,41 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# SPDX-License-Identifier: MIT + +param( + $SdkVersion = $(throw "SdkVersion not provided"), + $NewLibraryVersion = $(throw "NewLibraryVersion not provided") +) + +$SrcFolder = "..\src" +$LibConfigFile = "..\library.properties" + +Write-Host "Cloning azure-sdk-for-c repository." + +git clone -b $SdkVersion https://github.com/Azure/azure-sdk-for-c sdkrepo + +Write-Host "Flattening the azure-sdk-for-c file structure and updating src/." + +# Filtering out files not needed/supported on Arduino. +$Files = gci -Recurse -Include *.h, *.c .\sdkrepo\sdk | ?{ $_.DirectoryName -INOTMATCH "tests|sample" -AND $_.Name -INOTMATCH "curl|win32|az_posix" } + +rm -Force -Exclude "azure_ca.h" $SrcFolder/* + +copy -Verbose $Files $SrcFolder + +# Fixing headers to work as a flat structure. +Get-ChildItem -Recurse -Include *.c,*.h -Path $SrcFolder | %{ + $(Get-Content -Raw $_ ) -replace "