diff --git a/communication/inc/coap_api.h b/communication/inc/coap_api.h new file mode 100644 index 0000000000..0759832375 --- /dev/null +++ b/communication/inc/coap_api.h @@ -0,0 +1,522 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include + +/** + * Maximum size of payload data that can be sent or received without splitting the request or + * response message into multiple CoAP messages. + */ +#define COAP_BLOCK_SIZE 1024 + +/** + * Invalid request ID. + */ +#define COAP_INVALID_REQUEST_ID 0 + +#define COAP_CODE(_class, _detail) \ + (((_class & 0x07) << 5) | (_detail & 0x1f)) + +#define COAP_CODE_CLASS(_code) \ + ((_code >> 5) & 0x07) + +#define COAP_CODE_DETAIL(_code) \ + (_code & 0x1f) + +/** + * Message. + */ +typedef struct coap_message coap_message; + +/** + * Message option. + */ +typedef struct coap_option coap_option; + +/** + * Callback invoked when the status of the CoAP connection changes. + * + * @param error 0 if the status changed normally, otherwise an error code defined by the + * `system_error_t` enum. + * @param status Current status as defined by the `coap_connection_status` enum. + * @param arg User argument. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +typedef int (*coap_connection_callback)(int error, int status, void* arg); + +/** + * Callback invoked when a request message is received. + * + * The message instance must be destroyed by calling `coap_destroy_message()` when it's no longer + * needed. + * + * @param msg Request message. + * @param uri Request URI. + * @param method Method code as defined by the `coap_method` enum. + * @param req_id Request ID. + * @param arg User argument. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +typedef int (*coap_request_callback)(coap_message* msg, const char* uri, int method, int req_id, void* arg); + +/** + * Callback invoked when a response message is received. + * + * The message instance must be destroyed by calling `coap_destroy_message()` when it's no longer + * needed. + * + * @param msg Response message. + * @param status Response code as defined by the `coap_status` enum. + * @param req_id ID of the request for which this response is being received. + * @param arg User argument. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +typedef int (*coap_response_callback)(coap_message* msg, int status, int req_id, void* arg); + +/** + * Callback invoked when a block of a request or response message has been sent or received. + * + * @param msg Request or response message. + * @param req_id ID of the request that started the message exchange. + * @param arg User argument. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +typedef int (*coap_block_callback)(coap_message* msg, int req_id, void* arg); + +/** + * Callback invoked when a request or response message is acknowledged. + * + * @param req_id ID of the request that started the message exchange. + * @param arg User argument. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +typedef int (*coap_ack_callback)(int req_id, void* arg); + +/** + * Callback invoked when an error occurs while sending a request or response message. + * + * @param error Error code as defined by the `system_error_t` enum. + * @param req_id ID of the request that started the failed message exchange. + * @param arg User argument. + */ +typedef void (*coap_error_callback)(int error, int req_id, void* arg); + +/** + * Method code. + */ +typedef enum coap_method { + COAP_METHOD_GET = COAP_CODE(0, 1), ///< GET method. + COAP_METHOD_POST = COAP_CODE(0, 2), ///< POST method. + COAP_METHOD_PUT = COAP_CODE(0, 3), ///< PUT method. + COAP_METHOD_DELETE = COAP_CODE(0, 4), ///< DELETE method. +} coap_method; + +/** + * Response code. + */ +typedef enum coap_status { + // Success 2.xx + COAP_STATUS_CREATED = COAP_CODE(2, 1), ///< 2.01 Created. + COAP_STATUS_DELETED = COAP_CODE(2, 2), ///< 2.02 Deleted. + COAP_STATUS_VALID = COAP_CODE(2, 3), ///< 2.03 Valid. + COAP_STATUS_CHANGED = COAP_CODE(2, 4), ///< 2.04 Changed. + COAP_STATUS_CONTENT = COAP_CODE(2, 5), ///< 2.05 Content. + // Client Error 4.xx + COAP_STATUS_BAD_REQUEST = COAP_CODE(4, 0), ///< 4.00 Bad Request. + COAP_STATUS_UNAUTHORIZED = COAP_CODE(4, 1), ///< 4.01 Unauthorized. + COAP_STATUS_BAD_OPTION = COAP_CODE(4, 2), ///< 4.02 Bad Option. + COAP_STATUS_FORBIDDEN = COAP_CODE(4, 3), ///< 4.03 Forbidden. + COAP_STATUS_NOT_FOUND = COAP_CODE(4, 4), ///< 4.04 Not Found. + COAP_STATUS_METHOD_NOT_ALLOWED = COAP_CODE(4, 5), ///< 4.05 Method Not Allowed. + COAP_STATUS_NOT_ACCEPTABLE = COAP_CODE(4, 6), ///< 4.06 Not Acceptable. + COAP_STATUS_PRECONDITION_FAILED = COAP_CODE(4, 12), ///< 4.12 Precondition Failed. + COAP_STATUS_REQUEST_ENTITY_TOO_LARGE = COAP_CODE(4, 13), ///< 4.13 Request Entity Too Large. + COAP_STATUS_UNSUPPORTED_CONTENT_FORMAT = COAP_CODE(4, 15), ///< 4.15 Unsupported Content-Format. + // Server Error 5.xx + COAP_STATUS_INTERNAL_SERVER_ERROR = COAP_CODE(5, 0), ///< 5.00 Internal Server Error. + COAP_STATUS_NOT_IMPLEMENTED = COAP_CODE(5, 1), ///< 5.01 Not Implemented. + COAP_STATUS_BAD_GATEWAY = COAP_CODE(5, 2), ///< 5.02 Bad Gateway. + COAP_STATUS_SERVICE_UNAVAILABLE = COAP_CODE(5, 3), ///< 5.03 Service Unavailable. + COAP_STATUS_GATEWAY_TIMEOUT = COAP_CODE(5, 4), ///< 5.04 Gateway Timeout. + COAP_STATUS_PROXYING_NOT_SUPPORTED = COAP_CODE(5, 5) ///< 5.05 Proxying Not Supported. +} coap_status; + +/** + * Option number. + */ +typedef enum coap_option_number { + COAP_OPTION_IF_MATCH = 1, ///< If-Match. + COAP_OPTION_URI_HOST = 3, ///< Uri-Host. + COAP_OPTION_ETAG = 4, ///< ETag. + COAP_OPTION_IF_NONE_MATCH = 5, ///< If-None-Match. + COAP_OPTION_URI_PORT = 7, ///< Uri-Port. + COAP_OPTION_LOCATION_PATH = 8, ///< Location-Path. + COAP_OPTION_URI_PATH = 11, ///< Uri-Path. + COAP_OPTION_CONTENT_FORMAT = 12, ///< Content-Format. + COAP_OPTION_MAX_AGE = 14, ///< Max-Age. + COAP_OPTION_URI_QUERY = 15, ///< Uri-Query. + COAP_OPTION_ACCEPT = 17, ///< Accept. + COAP_OPTION_LOCATION_QUERY = 20, ///< Location-Query. + COAP_OPTION_PROXY_URI = 35, ///< Proxy-Uri. + COAP_OPTION_PROXY_SCHEME = 39, ///< Proxy-Scheme. + COAP_OPTION_SIZE1 = 60 ///< Size1. +} coap_option_number; + +/** + * Content format. + * + * https://www.iana.org/assignments/core-parameters/core-parameters.xhtml#content-formats + */ +typedef enum coap_format { + COAP_FORMAT_TEXT_PLAIN = 0, // text/plain; charset=utf-8 + COAP_FORMAT_OCTET_STREAM = 42, // application/octet-stream + COAP_FORMAT_JSON = 50, // application/json + COAP_FORMAT_CBOR = 60 // application/cbor +} coap_format; + +/** + * Connection status. + */ +typedef enum coap_connection_status { + COAP_CONNECTION_CLOSED = 0, ///< Connection is closed. + COAP_CONNECTION_OPEN = 1 ///< Connection is open. +} coap_connection_status; + +/** + * Result code. + */ +typedef enum coap_result { + COAP_RESULT_WAIT_BLOCK = 1 ///< Waiting for the next block of the message to be sent or received. +} coap_result; + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Register a connection handler. + * + * @param cb Handler callback. + * @param arg User argument to pass to the callback. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_add_connection_handler(coap_connection_callback cb, void* arg, void* reserved); + +/** + * Unregister a connection handler. + * + * @param cb Handler callback. + * @param reserved Reserved argument. Must be set to `NULL`. + */ +void coap_remove_connection_handler(coap_connection_callback cb, void* reserved); + +/** + * Register a handler for incoming requests. + * + * If a handler is already registered for the given combination of URI prefix and method code, it + * will be replaced. + * + * @param uri URI prefix. + * @param method Method code as defined by the `coap_method` enum. + * @param flags Reserved argument. Must be set to 0. + * @param cb Handler callback. + * @param arg User argument to pass to the callback. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_add_request_handler(const char* uri, int method, int flags, coap_request_callback cb, void* arg, void* reserved); + +/** + * Unregister a handler for incoming requests. + * + * @param uri URI prefix. + * @param method Method code as defined by the `coap_method` enum. + * @param reserved Reserved argument. Must be set to `NULL`. + */ +void coap_remove_request_handler(const char* uri, int method, void* reserved); + +/** + * Begin sending a request message. + * + * @param[out] msg Request message. + * @param uri Request URI. + * @param method Method code as defined by the `coap_method` enum. + * @param timeout Request timeout in milliseconds. If 0, the default timeout is used. + * @param flags Reserved argument. Must be set to 0. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return ID of the request on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_begin_request(coap_message** msg, const char* uri, int method, int timeout, int flags, void* reserved); + +/** + * Finish sending a request message. + * + * If the function call succeeds, the message instance must not be used again with any of the + * functions of this API. + * + * @param msg Request message. + * @param resp_cb Callback to invoke when a response for this request is received. Can be `NULL`. + * @param ack_cb Callback to invoke when the request is acknowledged. Can be `NULL`. + * @param error_cb Callback to invoke when an error occurs while sending the request. Can be `NULL`. + * @param arg User argument to pass to the callbacks. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_end_request(coap_message* msg, coap_response_callback resp_cb, coap_ack_callback ack_cb, + coap_error_callback error_cb, void* arg, void* reserved); + +/** + * Begin sending a response message. + * + * @param[out] msg Response message. + * @param status Response code as defined by the `coap_status` enum. + * @param req_id ID of the request which this response is meant for. + * @param flags Reserved argument. Must be set to 0. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_begin_response(coap_message** msg, int status, int req_id, int flags, void* reserved); + +/** + * Finish sending a response message. + * + * If the function call succeeds, the message instance must not be used again with any of the + * functions of this API. + * + * @param msg Response message. + * @param ack_cb Callback to invoke when the response is acknowledged. Can be `NULL`. + * @param error_cb Callback to invoke when an error occurs while sending the response. Can be `NULL`. + * @param arg User argument to pass to the callbacks. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_end_response(coap_message* msg, coap_ack_callback ack_cb, coap_error_callback error_cb, + void* arg, void* reserved); + +/** + * Destroy a message. + * + * Destroying an outgoing request or response message before `coap_end_request()` or + * `coap_end_response()` is called cancels the message exchange. + * + * @param msg Request or response message. + * @param reserved Reserved argument. Must be set to `NULL`. + */ +void coap_destroy_message(coap_message* msg, void* reserved); + +/** + * Cancel an ongoing request. + * + * Cancelling a request prevents any callbacks associated with the respective message exchange from + * being invoked by the API. + * + * If the caller still owns a message instance associated with the given exchange, it needs to be + * destroyed via `coap_destroy_message()`. + * + * @param req_id ID of the request that started the message exchange. + * @param reserved Reserved argument. Must be set to `NULL`. + */ +void coap_cancel_request(int req_id, void* reserved); + +/** + * Write the payload data of a message. + * + * If the data can't be sent in one message block, the function will return `COAP_RESULT_WAIT_BLOCK`. + * When that happens, the caller must stop writing the payload data until the `block_cb` callback is + * invoked by the system to notify the caller that the next block of the message can be sent. + * + * All message options must be set prior to writing the payload data. + * + * @param msg Request or response message. + * @param data Input buffer. + * @param[in,out] size **in:** Number of bytes to write. + * **out:** Number of bytes written. + * @param block_cb Callback to invoke when the next block of the message can be sent. Can be `NULL`. + * @param error_cb Callback to invoke when an error occurs while sending the current block of the + * message. Can be `NULL`. + * @param arg User argument to pass to the callbacks. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 or `COAP_RESULT_WAIT_BLOCK` on success, otherwise an error code defined by the + * `system_error_t` enum. + */ +int coap_write_payload(coap_message* msg, const char* data, size_t* size, coap_block_callback block_cb, + coap_error_callback error_cb, void* arg, void* reserved); + +/** + * Read the payload data of a message. + * + * If the end of the current message block is reached and more blocks are expected to be received for + * this message, the function will return `COAP_RESULT_WAIT_BLOCK`. The `block_cb` callback will be + * invoked by the system to notify the caller that the next message block is available for reading. + * + * @param msg Request or response message. + * @param[out] data Output buffer. Can be `NULL`. + * @param[in,out] size **in:** Number of bytes to read. + * **out:** Number of bytes read. + * @param block_cb Callback to invoke when the next block of the message is received. Can be `NULL`. + * @param error_cb Callback to invoke when an error occurs while receiving the next block of the + * message. Can be `NULL`. + * @param arg User argument to pass to the callbacks. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 or `COAP_RESULT_WAIT_BLOCK` on success, otherwise an error code defined by the + * `system_error_t` enum. + */ +int coap_read_payload(coap_message* msg, char* data, size_t *size, coap_block_callback block_cb, + coap_error_callback error_cb, void* arg, void* reserved); + +/** + * Read the payload data of the current message block without changing the reading position in it. + * + * @param msg Request or response message. + * @param[out] data Output buffer. Can be `NULL`. + * @param size Number of bytes to read. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return Number of bytes read or an error code defined by the `system_error_t` enum. + */ +int coap_peek_payload(coap_message* msg, char* data, size_t size, void* reserved); + +/** + * Get a message option. + * + * @param[out] opt Message option. If the option with the given number cannot be found, the argument + * is set to `NULL`. + * @param num Option number. + * @param msg Request or response message. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_get_option(coap_option** opt, int num, coap_message* msg, void* reserved); + +/** + * Get the next message option. + * + * @param[in,out] opt **in:** Current option. If `NULL`, the first option of the message is returned. + * **out:** Next option. If `NULL`, the option provided is the last option of the message. + * @param[out] num Option number. + * @param msg Request or response message. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_get_next_option(coap_option** opt, int* num, coap_message* msg, void* reserved); + +/** + * Get the value of an `uint` option. + * + * @param opt Message option. + * @param[out] val Option value. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_get_uint_option_value(const coap_option* opt, unsigned* val, void* reserved); + +/** + * Get the value of an `uint` option as a 64-bit integer. + * + * @param opt Message option. + * @param[out] val Option value. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_get_uint64_option_value(const coap_option* opt, uint64_t* val, void* reserved); + +/** + * Get the value of a string option. + * + * The output is null-terminated unless the size of the output buffer is 0. + * + * @param opt Message option. + * @param data Output buffer. Can be `NULL`. + * @param size Size of the buffer. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return On success, the actual size of the option value not including the terminating null (can + * be greater than `size`). Otherwise, an error code defined by the `system_error_t` enum. + */ +int coap_get_string_option_value(const coap_option* opt, char* data, size_t size, void* reserved); + +/** + * Get the value of an opaque option. + * + * @param opt Message option. + * @param data Output buffer. Can be `NULL`. + * @param size Size of the buffer. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return On success, the actual size of the option value (can be greater than `size`). Otherwise, + * an error code defined by the `system_error_t` enum. + */ +int coap_get_opaque_option_value(const coap_option* opt, char* data, size_t size, void* reserved); + +/** + * Add an empty option to a message. + * + * @param msg Request of response message. + * @param num Option number. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_add_empty_option(coap_message* msg, int num, void* reserved); + +/** + * Add a `uint` option to a message. + * + * @param msg Request of response message. + * @param num Option number. + * @param val Option value. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_add_uint_option(coap_message* msg, int num, unsigned val, void* reserved); + +/** + * Add a `uint` option to a message as a 64-bit integer. + * + * @param msg Request of response message. + * @param num Option number. + * @param val Option value. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_add_uint64_option(coap_message* msg, int num, uint64_t val, void* reserved); + +/** + * Add a string option to a message. + * + * @param msg Request of response message. + * @param num Option number. + * @param str Option value. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_add_string_option(coap_message* msg, int num, const char* str, void* reserved); + +/** + * Add an opaque option to a message. + * + * @param msg Request of response message. + * @param num Option number. + * @param data Option data. + * @param size Size of the option data. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int coap_add_opaque_option(coap_message* msg, int num, const char* data, size_t size, void* reserved); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/communication/inc/coap_channel_new.h b/communication/inc/coap_channel_new.h new file mode 100644 index 0000000000..4067d694a9 --- /dev/null +++ b/communication/inc/coap_channel_new.h @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#include +#include + +#include "message_channel.h" +#include "coap_api.h" +#include "coap.h" // For token_t + +#include "system_tick_hal.h" + +#include "ref_count.h" + +namespace particle::protocol { + +class CoapMessageDecoder; +class Protocol; + +namespace experimental { + +// This class implements the new experimental protocol API that allows the system to interact with +// the server at the CoAP level. It's meant to be used through the functions defined in coap_api.h +class CoapChannel { +public: + enum Result { + HANDLED = 1 // Returned by the handle* methods + }; + + ~CoapChannel(); + + // Methods called by the new CoAP API (coap_api.h) + + int beginRequest(coap_message** msg, const char* uri, coap_method method, int timeout); + int endRequest(coap_message* msg, coap_response_callback respCallback, coap_ack_callback ackCallback, + coap_error_callback errorCallback, void* callbackArg); + + int beginResponse(coap_message** msg, int code, int requestId); + int endResponse(coap_message* msg, coap_ack_callback ackCallback, coap_error_callback errorCallback, + void* callbackArg); + + int writePayload(coap_message* msg, const char* data, size_t& size, coap_block_callback blockCallback, + coap_error_callback errorCallback, void* callbackArg); + int readPayload(coap_message* msg, char* data, size_t& size, coap_block_callback blockCallback, + coap_error_callback errorCallback, void* callbackArg); + int peekPayload(coap_message* msg, char* data, size_t size); + + void destroyMessage(coap_message* msg); + + void cancelRequest(int requestId); + + int addRequestHandler(const char* uri, coap_method method, coap_request_callback callback, void* callbackArg); + void removeRequestHandler(const char* uri, coap_method method); + + int addConnectionHandler(coap_connection_callback callback, void* callbackArg); + void removeConnectionHandler(coap_connection_callback callback); + + // Methods called by the old protocol implementation + + void open(); + void close(int error = SYSTEM_ERROR_COAP_CONNECTION_CLOSED); + + int handleCon(const Message& msg); + int handleAck(const Message& msg); + int handleRst(const Message& msg); + + int run(); + + static CoapChannel* instance(); + +private: + // Channel state + enum class State { + CLOSED, + OPENING, + OPEN, + CLOSING + }; + + enum class MessageType { + REQUEST, // Regular or blockwise request carrying request data + BLOCK_REQUEST, // Blockwise request retrieving a block of response data + RESPONSE // Regular or blockwise response + }; + + enum class MessageState { + NEW, // Message created + READ, // Reading payload data + WRITE, // Writing payload data + WAIT_ACK, // Waiting for an ACK + WAIT_RESPONSE, // Waiting for a response + WAIT_BLOCK, // Waiting for the next message block + DONE // Message exchange completed + }; + + struct CoapMessage; + struct RequestMessage; + struct ResponseMessage; + struct RequestHandler; + struct ConnectionHandler; + + CoapChannel(); // Use instance() + + Message msgBuf_; // Reference to the shared message buffer + ConnectionHandler* connHandlers_; // List of registered connection handlers + RequestHandler* reqHandlers_; // List of registered request handlers + RequestMessage* sentReqs_; // List of requests awaiting a response from the server + RequestMessage* recvReqs_; // List of requests awaiting a response from the device + ResponseMessage* blockResps_; // List of responses for which the next message block is expected to be received + CoapMessage* unackMsgs_; // List of messages awaiting an ACK from the server + Protocol* protocol_; // Protocol instance + State state_; // Channel state + uint32_t lastReqTag_; // Last used request tag + int lastMsgId_; // Last used internal message ID + int curMsgId_; // Internal ID of the message stored in the shared buffer + int sessId_; // Counter incremented every time a new session with the server is started + int pendingCloseError_; // If non-zero, the channel needs to be closed + bool openPending_; // If true, the channel needs to be reopened + + int handleRequest(CoapMessageDecoder& d); + int handleResponse(CoapMessageDecoder& d); + int handleAck(CoapMessageDecoder& d); + + int prepareMessage(const RefCountPtr& msg); + int updateMessage(const RefCountPtr& msg); + int sendMessage(RefCountPtr msg); + void clearMessage(const RefCountPtr& msg); + + int sendAck(int coapId, bool rst = false); + + int handleProtocolError(ProtocolError error); + + void releaseMessageBuffer(); + + system_tick_t millis() const; +}; + +} // namespace experimental + +} // namespace particle::protocol diff --git a/communication/inc/coap_util.h b/communication/inc/coap_util.h new file mode 100644 index 0000000000..f3824402fc --- /dev/null +++ b/communication/inc/coap_util.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2022 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include "coap_api.h" +#include "logging.h" + +namespace particle { + +/** + * Smart pointer for CoAP message instances. + */ +class CoapMessagePtr { +public: + CoapMessagePtr() : + msg_(nullptr) { + } + + explicit CoapMessagePtr(coap_message* msg) : + msg_(msg) { + } + + CoapMessagePtr(const CoapMessagePtr&) = delete; + + CoapMessagePtr(CoapMessagePtr&& ptr) : + msg_(ptr.msg_) { + ptr.msg_ = nullptr; + } + + ~CoapMessagePtr() { + coap_destroy_message(msg_, nullptr); + } + + coap_message* get() const { + return msg_; + } + + coap_message* release() { + auto msg = msg_; + msg_ = nullptr; + return msg; + } + + void reset(coap_message* msg = nullptr) { + coap_destroy_message(msg_, nullptr); + msg_ = msg; + } + + CoapMessagePtr& operator=(const CoapMessagePtr&) = delete; + + CoapMessagePtr& operator=(CoapMessagePtr&& ptr) { + coap_destroy_message(msg_, nullptr); + msg_ = ptr.msg_; + ptr.msg_ = nullptr; + return *this; + } + + explicit operator bool() const { + return msg_; + } + + friend void swap(CoapMessagePtr& ptr1, CoapMessagePtr& ptr2) { + auto msg = ptr1.msg_; + ptr1.msg_ = ptr2.msg_; + ptr2.msg_ = msg; + } + +private: + coap_message* msg_; +}; + +namespace protocol { + +/** + * Log the contents of a CoAP message. + * + * @param level Logging level. + * @param category Logging category. + * @param data Message data. + * @param size Message size. + * @param logPayload Whether to log the payload data of the message. + */ +void logCoapMessage(LogLevel level, const char* category, const char* data, size_t size, bool logPayload = false); + +} // namespace protocol + +} // namespace particle diff --git a/communication/inc/protocol_defs.h b/communication/inc/protocol_defs.h index 6124815618..6885c6d2f3 100644 --- a/communication/inc/protocol_defs.h +++ b/communication/inc/protocol_defs.h @@ -60,6 +60,7 @@ enum ProtocolError IO_ERROR_SOCKET_SEND_FAILED = 33, IO_ERROR_SOCKET_RECV_FAILED = 34, IO_ERROR_REMOTE_END_CLOSED = 35, + COAP_ERROR = 36, // NOTE: when adding more ProtocolError codes, be sure to update toSystemError() in protocol_defs.cpp UNKNOWN = 0x7FFFF }; diff --git a/communication/src/build.mk b/communication/src/build.mk index 4a3a971300..c6bc89ecc4 100644 --- a/communication/src/build.mk +++ b/communication/src/build.mk @@ -38,6 +38,7 @@ CPPSRC += $(TARGET_SRC_PATH)/coap_message_decoder.cpp CPPSRC += $(TARGET_SRC_PATH)/coap_util.cpp CPPSRC += $(TARGET_SRC_PATH)/firmware_update.cpp CPPSRC += $(TARGET_SRC_PATH)/description.cpp +CPPSRC += $(TARGET_SRC_PATH)/coap_channel_new.cpp # ASM source files included in this build. ASRC += diff --git a/communication/src/coap_channel.cpp b/communication/src/coap_channel.cpp index 46fb6128fe..fdffcbb61a 100644 --- a/communication/src/coap_channel.cpp +++ b/communication/src/coap_channel.cpp @@ -20,9 +20,11 @@ #undef LOG_COMPILE_TIME_LEVEL #include "coap_channel.h" +#include "coap_channel_new.h" #include "service_debug.h" #include "messages.h" #include "communication_diagnostic.h" +#include "system_error.h" namespace particle { namespace protocol { @@ -64,6 +66,9 @@ void CoAPMessageStore::message_timeout(CoAPMessage& msg, Channel& channel) if (msg.is_request()) { LOG(ERROR, "CoAP message timeout; ID: %d", (int)msg.get_id()); g_unacknowledgedMessageCounter++; + // XXX: This will cancel _all_ messages with a timeout error, not just the timed out one. + // That's not ideal but should be okay while we're transitioning to the new CoAP API + experimental::CoapChannel::instance()->close(SYSTEM_ERROR_COAP_TIMEOUT); channel.command(MessageChannel::CLOSE); } } @@ -141,6 +146,7 @@ ProtocolError CoAPMessageStore::receive(Message& msg, Channel& channel, system_t } if (msgtype==CoAPType::RESET) { LOG(WARN, "Received RST message; discarding session"); + experimental::CoapChannel::instance()->handleRst(msg); if (coap_msg) { coap_msg->notify_delivered_nak(); } diff --git a/communication/src/coap_channel_new.cpp b/communication/src/coap_channel_new.cpp new file mode 100644 index 0000000000..3cdce9f6ac --- /dev/null +++ b/communication/src/coap_channel_new.cpp @@ -0,0 +1,1474 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#if !defined(DEBUG_BUILD) && !defined(UNIT_TEST) +#define NDEBUG // TODO: Define NDEBUG in release builds +#endif + +#include "logging.h" + +LOG_SOURCE_CATEGORY("system.coap") + +#include +#include +#include +#include + +#include "coap_channel_new.h" + +#include "coap_message_encoder.h" +#include "coap_message_decoder.h" +#include "protocol.h" +#include "spark_protocol_functions.h" + +#include "random.h" +#include "scope_guard.h" +#include "check.h" + +#define CHECK_PROTOCOL(_expr) \ + do { \ + auto _r = _expr; \ + if (_r != ::particle::protocol::ProtocolError::NO_ERROR) { \ + return this->handleProtocolError(_r); \ + } \ + } while (false) + +namespace particle::protocol::experimental { + +namespace { + +const size_t MAX_TOKEN_SIZE = sizeof(token_t); // TODO: Support longer tokens +const size_t MAX_TAG_SIZE = 8; // Maximum size of an ETag (RFC 7252) or Request-Tag (RFC 9175) option + +const unsigned BLOCK_SZX = 6; // Value of the SZX field for 1024-byte blocks (RFC 7959, 2.2) +static_assert(COAP_BLOCK_SIZE == 1024); // When changing the block size, make sure to update BLOCK_SZX accordingly + +const unsigned DEFAULT_REQUEST_TIMEOUT = 60000; + +static_assert(COAP_INVALID_REQUEST_ID == 0); // Used by value in the code + +unsigned encodeBlockOption(int num, bool m) { + unsigned opt = (num << 4) | BLOCK_SZX; + if (m) { + opt |= 0x08; + } + return opt; +} + +int decodeBlockOption(unsigned opt, int& num, bool& m) { + unsigned szx = opt & 0x07; + if (szx != BLOCK_SZX) { + // Server is required to use exactly the same block size + return SYSTEM_ERROR_NOT_SUPPORTED; + } + num = opt >> 4; + m = opt & 0x08; + return 0; +} + +bool isValidCoapMethod(int method) { + switch (method) { + case COAP_METHOD_GET: + case COAP_METHOD_POST: + case COAP_METHOD_PUT: + case COAP_METHOD_DELETE: + return true; + default: + return false; + } +} + +// TODO: Use a generic intrusive list container +template>> +inline void addToList(T*& head, E* elem) { + assert(!elem->next && !elem->prev); // Debug-only + elem->prev = nullptr; + elem->next = head; + if (head) { + head->prev = elem; + } + head = elem; +} + +template || std::is_base_of_v>> +inline void removeFromList(T*& head, E* elem) { + assert(elem->next || elem->prev); + if (elem->prev) { + assert(head != elem); + elem->prev->next = elem->next; + } else { + assert(head == elem); + head = static_cast(elem->next); + } + if (elem->next) { + elem->next->prev = elem->prev; + } +#ifndef NDEBUG + elem->next = nullptr; + elem->prev = nullptr; +#endif +} + +template +inline T* findInList(T* head, const F& fn) { + while (head) { + if (fn(head)) { + return head; + } + head = static_cast(head->next); + } + return nullptr; +} + +// Invokes a function object for each element in the list. It's not safe to use this function if the +// function object may delete multiple elements of the list and not just the element for which it +// was called +template +inline void forEachInList(T* head, const F& fn) { + while (head) { + // Store the pointer to the next element in case the callback deletes the current element + auto next = head->next; + fn(head); + head = static_cast(next); + } +} + +// Adds a reference-counted object to a list and increments the reference counter +template +inline void addRefToList(T*& head, RefCountPtr elem) { + addToList(head, elem.unwrap()); +} + +// Removes a reference counted object from the list and decrements the reference counter +template +inline void removeRefFromList(T*& head, const RefCountPtr& elem) { + removeFromList(head, elem.get()); + // Release the unmanaged reference to the object + elem->release(); +} + +template +inline RefCountPtr findRefInList(T* head, const F& fn) { + return findInList(head, fn); +} + +template +inline void forEachRefInList(T* head, const F& fn) { + while (head) { + // Prevent the object from being deleted by the callback + RefCountPtr elem(head); + fn(elem.get()); + head = static_cast(elem->next); + } +} + +} // namespace + +struct CoapChannel::CoapMessage: RefCount { + coap_block_callback blockCallback; // Callback to invoke when a message block is sent or received + coap_ack_callback ackCallback; // Callback to invoke when the message is acknowledged + coap_error_callback errorCallback; // Callback to invoke when an error occurs + void* callbackArg; // User argument to pass to the callbacks + + int id; // Internal message ID + int requestId; // Internal ID of the request that started this message exchange + int sessionId; // ID of the session for which this message was created + + char tag[MAX_TAG_SIZE]; // ETag or Request-Tag option + size_t tagSize; // Size of the ETag or Request-Tag option + int coapId; // CoAP message ID + token_t token; // CoAP token. TODO: Support longer tokens + + std::optional blockIndex; // Index of the current message block + std::optional hasMore; // Whether more blocks are expected for this message + + char* pos; // Current position in the message buffer. If null, no message data has been written to the buffer yet + char* end; // End of the message buffer + size_t prefixSize; // Size of the CoAP framing not including the payload marker + + MessageType type; // Message type + MessageState state; // Message state + + CoapMessage* next; // Next message in the list + CoapMessage* prev; // Previous message in the list + + explicit CoapMessage(MessageType type) : + blockCallback(nullptr), + ackCallback(nullptr), + errorCallback(nullptr), + callbackArg(nullptr), + id(0), + requestId(0), + sessionId(0), + tag(), + tagSize(0), + coapId(0), + token(0), + pos(nullptr), + end(nullptr), + prefixSize(0), + type(type), + state(MessageState::NEW), + next(nullptr), + prev(nullptr) { + } +}; + +// TODO: Use separate message classes for different transfer directions +struct CoapChannel::RequestMessage: CoapMessage { + coap_response_callback responseCallback; // Callback to invoke when a response for this request is received + + ResponseMessage* blockResponse; // Response for which this block request is retrieving data + + system_tick_t timeSent; // Time the request was sent + unsigned timeout; // Request timeout + + coap_method method; // CoAP method code + char uri; // Request URI. TODO: Support longer URIs + + explicit RequestMessage(bool blockRequest = false) : + CoapMessage(blockRequest ? MessageType::BLOCK_REQUEST : MessageType::REQUEST), + responseCallback(nullptr), + blockResponse(nullptr), + timeSent(0), + timeout(0), + method(), + uri(0) { + } +}; + +struct CoapChannel::ResponseMessage: CoapMessage { + RefCountPtr blockRequest; // Request message used to get the last received block of this message + + int status; // CoAP response code + + ResponseMessage() : + CoapMessage(MessageType::RESPONSE), + status(0) { + } +}; + +struct CoapChannel::RequestHandler { + coap_request_callback callback; // Callback to invoke when a request is received + void* callbackArg; // User argument to pass to the callback + + coap_method method; // CoAP method code + char uri; // Request URI. TODO: Support longer URIs + + RequestHandler* next; // Next handler in the list + RequestHandler* prev; // Previous handler in the list + + RequestHandler(char uri, coap_method method, coap_request_callback callback, void* callbackArg) : + callback(callback), + callbackArg(callbackArg), + method(method), + uri(uri), + next(nullptr), + prev(nullptr) { + } +}; + +struct CoapChannel::ConnectionHandler { + coap_connection_callback callback; // Callback to invoke when the connection status changes + void* callbackArg; // User argument to pass to the callback + + bool openFailed; // If true, the user callback returned an error when the connection was opened + + ConnectionHandler* next; // Next handler in the list + ConnectionHandler* prev; // Previous handler in the list + + ConnectionHandler(coap_connection_callback callback, void* callbackArg) : + callback(callback), + callbackArg(callbackArg), + openFailed(false), + next(nullptr), + prev(nullptr) { + } +}; + +CoapChannel::CoapChannel() : + connHandlers_(nullptr), + reqHandlers_(nullptr), + sentReqs_(nullptr), + recvReqs_(nullptr), + blockResps_(nullptr), + unackMsgs_(nullptr), + protocol_(spark_protocol_instance()), + state_(State::CLOSED), + lastReqTag_(Random().gen()), + lastMsgId_(0), + curMsgId_(0), + sessId_(0), + pendingCloseError_(0), + openPending_(false) { +} + +CoapChannel::~CoapChannel() { + if (sentReqs_ || recvReqs_ || blockResps_ || unackMsgs_) { + LOG(ERROR, "Destroying channel while CoAP exchange is in progress"); + } + close(); + forEachInList(connHandlers_, [](auto h) { + delete h; + }); + forEachInList(reqHandlers_, [](auto h) { + delete h; + }); +} + +int CoapChannel::beginRequest(coap_message** msg, const char* uri, coap_method method, int timeout) { + if (timeout < 0 || std::strlen(uri) != 1) { // TODO: Support longer URIs + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + if (state_ != State::OPEN) { + return SYSTEM_ERROR_COAP_CONNECTION_CLOSED; + } + auto req = makeRefCountPtr(); + if (!req) { + return SYSTEM_ERROR_NO_MEMORY; + } + auto msgId = ++lastMsgId_; + req->id = msgId; + req->requestId = msgId; + req->sessionId = sessId_; + req->uri = *uri; + req->method = method; + req->timeout = (timeout > 0) ? timeout : DEFAULT_REQUEST_TIMEOUT; + req->state = MessageState::WRITE; + // Transfer ownership over the message to the calling code + *msg = reinterpret_cast(req.unwrap()); + return msgId; +} + +int CoapChannel::endRequest(coap_message* apiMsg, coap_response_callback respCallback, coap_ack_callback ackCallback, + coap_error_callback errorCallback, void* callbackArg) { + auto msg = RefCountPtr(reinterpret_cast(apiMsg)); + if (msg->type != MessageType::REQUEST) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + if (msg->sessionId != sessId_) { + return SYSTEM_ERROR_COAP_REQUEST_CANCELLED; + } + auto req = staticPtrCast(msg); + if (req->state != MessageState::WRITE || req->hasMore.value_or(false)) { + return SYSTEM_ERROR_INVALID_STATE; + } + if (!req->pos) { + if (curMsgId_) { + // TODO: Support asynchronous writing to multiple message instances + LOG(WARN, "CoAP message buffer is already in use"); + releaseMessageBuffer(); + } + CHECK(prepareMessage(req)); + } else if (curMsgId_ != req->id) { + LOG(ERROR, "CoAP message buffer is no longer available"); + return SYSTEM_ERROR_NOT_SUPPORTED; + } + CHECK(sendMessage(req)); + req->responseCallback = respCallback; + req->ackCallback = ackCallback; + req->errorCallback = errorCallback; + req->callbackArg = callbackArg; + // Take ownership over the message by releasing the reference to the message that was previously + // managed by the calling code + req->release(); + return 0; +} + +int CoapChannel::beginResponse(coap_message** msg, int status, int requestId) { + if (state_ != State::OPEN) { + return SYSTEM_ERROR_COAP_CONNECTION_CLOSED; + } + auto req = findRefInList(recvReqs_, [=](auto req) { + return req->id == requestId; + }); + if (!req) { + return SYSTEM_ERROR_COAP_REQUEST_NOT_FOUND; + } + auto resp = makeRefCountPtr(); + if (!resp) { + return SYSTEM_ERROR_NO_MEMORY; + } + resp->id = ++lastMsgId_; + resp->requestId = req->id; + resp->sessionId = sessId_; + resp->token = req->token; + resp->status = status; + resp->state = MessageState::WRITE; + // Transfer ownership over the message to the calling code + *msg = reinterpret_cast(resp.unwrap()); + return 0; +} + +int CoapChannel::endResponse(coap_message* apiMsg, coap_ack_callback ackCallback, coap_error_callback errorCallback, + void* callbackArg) { + auto msg = RefCountPtr(reinterpret_cast(apiMsg)); + if (msg->type != MessageType::RESPONSE) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + if (msg->sessionId != sessId_) { + return SYSTEM_ERROR_COAP_REQUEST_CANCELLED; + } + auto resp = staticPtrCast(msg); + if (resp->state != MessageState::WRITE) { + return SYSTEM_ERROR_INVALID_STATE; + } + if (!resp->pos) { + if (curMsgId_) { + // TODO: Support asynchronous writing to multiple message instances + LOG(WARN, "CoAP message buffer is already in use"); + releaseMessageBuffer(); + } + CHECK(prepareMessage(resp)); + } else if (curMsgId_ != resp->id) { + LOG(ERROR, "CoAP message buffer is no longer available"); + return SYSTEM_ERROR_NOT_SUPPORTED; + } + CHECK(sendMessage(resp)); + resp->ackCallback = ackCallback; + resp->errorCallback = errorCallback; + resp->callbackArg = callbackArg; + // Take ownership over the message by releasing the reference to the message that was previously + // managed by the calling code + resp->release(); + return 0; +} + +int CoapChannel::writePayload(coap_message* apiMsg, const char* data, size_t& size, coap_block_callback blockCallback, + coap_error_callback errorCallback, void* callbackArg) { + auto msg = RefCountPtr(reinterpret_cast(apiMsg)); + if (msg->sessionId != sessId_) { + return SYSTEM_ERROR_COAP_REQUEST_CANCELLED; + } + if (msg->state != MessageState::WRITE) { + return SYSTEM_ERROR_INVALID_STATE; + } + bool sendBlock = false; + if (size > 0) { + if (!msg->pos) { + if (curMsgId_) { + // TODO: Support asynchronous writing to multiple message instances + LOG(WARN, "CoAP message buffer is already in use"); + releaseMessageBuffer(); + } + if (msg->blockIndex.has_value()) { + // Writing another message block + assert(msg->type == MessageType::REQUEST); + ++msg->blockIndex.value(); + msg->hasMore = false; + } + CHECK(prepareMessage(msg)); + *msg->pos++ = 0xff; // Payload marker + } else if (curMsgId_ != msg->id) { + LOG(ERROR, "CoAP message buffer is no longer available"); + return SYSTEM_ERROR_NOT_SUPPORTED; + } + auto bytesToWrite = size; + if (msg->pos + bytesToWrite > msg->end) { + if (msg->type != MessageType::REQUEST || !blockCallback) { // TODO: Support blockwise device-to-cloud responses + return SYSTEM_ERROR_TOO_LARGE; + } + bytesToWrite = msg->end - msg->pos; + sendBlock = true; + } + std::memcpy(msg->pos, data, bytesToWrite); + msg->pos += bytesToWrite; + if (sendBlock) { + if (!msg->blockIndex.has_value()) { + msg->blockIndex = 0; + // Add a Request-Tag option + auto tag = ++lastReqTag_; + static_assert(sizeof(tag) <= sizeof(msg->tag)); + std::memcpy(msg->tag, &tag, sizeof(tag)); + msg->tagSize = sizeof(tag); + } + msg->hasMore = true; + CHECK(updateMessage(msg)); // Update or add blockwise transfer options to the message + CHECK(sendMessage(msg)); + } + size = bytesToWrite; + } + msg->blockCallback = blockCallback; + msg->errorCallback = errorCallback; + msg->callbackArg = callbackArg; + return sendBlock ? COAP_RESULT_WAIT_BLOCK : 0; +} + +int CoapChannel::readPayload(coap_message* apiMsg, char* data, size_t& size, coap_block_callback blockCallback, + coap_error_callback errorCallback, void* callbackArg) { + auto msg = RefCountPtr(reinterpret_cast(apiMsg)); + if (msg->sessionId != sessId_) { + return SYSTEM_ERROR_COAP_REQUEST_CANCELLED; + } + if (msg->state != MessageState::READ) { + return SYSTEM_ERROR_INVALID_STATE; + } + bool getBlock = false; + if (size > 0) { + if (msg->pos == msg->end) { + return SYSTEM_ERROR_END_OF_STREAM; + } + if (curMsgId_ != msg->id) { + // TODO: Support asynchronous reading from multiple message instances + LOG(ERROR, "CoAP message buffer is no longer available"); + return SYSTEM_ERROR_NOT_SUPPORTED; + } + auto bytesToRead = std::min(size, msg->end - msg->pos); + if (data) { + std::memcpy(data, msg->pos, bytesToRead); + } + msg->pos += bytesToRead; + if (msg->pos == msg->end) { + releaseMessageBuffer(); + if (msg->hasMore.value_or(false)) { + if (blockCallback) { + assert(msg->type == MessageType::RESPONSE); // TODO: Support cloud-to-device blockwise requests + auto resp = staticPtrCast(msg); + // Send a new request with the original options and updated block number + auto req = resp->blockRequest; + assert(req && req->blockIndex.has_value()); + ++req->blockIndex.value(); + CHECK(prepareMessage(req)); + CHECK(sendMessage(std::move(req))); + resp->state = MessageState::WAIT_BLOCK; + addRefToList(blockResps_, std::move(resp)); + getBlock = true; + } else { + LOG(WARN, "Incomplete read of blockwise response"); + } + } + } + size = bytesToRead; + } + msg->blockCallback = blockCallback; + msg->errorCallback = errorCallback; + msg->callbackArg = callbackArg; + return getBlock ? COAP_RESULT_WAIT_BLOCK : 0; +} + +int CoapChannel::peekPayload(coap_message* apiMsg, char* data, size_t size) { + auto msg = RefCountPtr(reinterpret_cast(apiMsg)); + if (msg->sessionId != sessId_) { + return SYSTEM_ERROR_COAP_REQUEST_CANCELLED; + } + if (msg->state != MessageState::READ) { + return SYSTEM_ERROR_INVALID_STATE; + } + if (size > 0) { + if (msg->pos == msg->end) { + return SYSTEM_ERROR_END_OF_STREAM; + } + if (curMsgId_ != msg->id) { + // TODO: Support asynchronous reading from multiple message instances + LOG(ERROR, "CoAP message buffer is no longer available"); + return SYSTEM_ERROR_NOT_SUPPORTED; + } + size = std::min(size, msg->end - msg->pos); + if (data) { + std::memcpy(data, msg->pos, size); + } + } + return size; +} + +void CoapChannel::destroyMessage(coap_message* apiMsg) { + if (!apiMsg) { + return; + } + auto msg = RefCountPtr::wrap(reinterpret_cast(apiMsg)); // Take ownership + clearMessage(msg); +} + +void CoapChannel::cancelRequest(int requestId) { + if (requestId <= 0) { + return; + } + // Search among the messages for which a user callback may still need to be invoked + RefCountPtr msg = findRefInList(sentReqs_, [=](auto req) { + return req->type != MessageType::BLOCK_REQUEST && req->id == requestId; + }); + if (!msg) { + msg = findRefInList(unackMsgs_, [=](auto msg) { + return msg->type != MessageType::BLOCK_REQUEST && msg->requestId == requestId; + }); + if (!msg) { + msg = findRefInList(blockResps_, [=](auto msg) { + return msg->requestId == requestId; + }); + } + } + if (msg) { + clearMessage(msg); + } +} + +int CoapChannel::addRequestHandler(const char* uri, coap_method method, coap_request_callback callback, void* callbackArg) { + if (!*uri || !callback) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + if (std::strlen(uri) != 1) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO: Support longer URIs + } + if (state_ != State::CLOSED) { + return SYSTEM_ERROR_INVALID_STATE; + } + auto h = findInList(reqHandlers_, [=](auto h) { + return h->uri == *uri && h->method; + }); + if (h) { + h->callback = callback; + h->callbackArg = callbackArg; + } else { + std::unique_ptr h(new(std::nothrow) RequestHandler(*uri, method, callback, callbackArg)); + if (!h) { + return SYSTEM_ERROR_NO_MEMORY; + } + addToList(reqHandlers_, h.release()); + } + return 0; +} + +void CoapChannel::removeRequestHandler(const char* uri, coap_method method) { + if (std::strlen(uri) != 1) { // TODO: Support longer URIs + return; + } + if (state_ != State::CLOSED) { + LOG(ERROR, "Cannot remove handler while channel is open"); + return; + } + auto h = findInList(reqHandlers_, [=](auto h) { + return h->uri == *uri && h->method == method; + }); + if (h) { + removeFromList(reqHandlers_, h); + delete h; + } +} + +int CoapChannel::addConnectionHandler(coap_connection_callback callback, void* callbackArg) { + if (!callback) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + if (state_ != State::CLOSED) { + return SYSTEM_ERROR_INVALID_STATE; + } + auto h = findInList(connHandlers_, [=](auto h) { + return h->callback == callback; + }); + if (!h) { + std::unique_ptr h(new(std::nothrow) ConnectionHandler(callback, callbackArg)); + if (!h) { + return SYSTEM_ERROR_NO_MEMORY; + } + addToList(connHandlers_, h.release()); + } + return 0; +} + +void CoapChannel::removeConnectionHandler(coap_connection_callback callback) { + if (state_ != State::CLOSED) { + LOG(ERROR, "Cannot remove handler while channel is open"); + return; + } + auto h = findInList(connHandlers_, [=](auto h) { + return h->callback == callback; + }); + if (h) { + removeFromList(connHandlers_, h); + delete h; + } +} + +void CoapChannel::open() { + pendingCloseError_ = 0; + if (state_ != State::CLOSED) { + if (state_ == State::CLOSING) { + // open() is being called from a connection handler + openPending_ = true; + } + return; + } + state_ = State::OPENING; + forEachInList(connHandlers_, [](auto h) { + assert(h->callback); + int r = h->callback(0 /* error */, COAP_CONNECTION_OPEN, h->callbackArg); + if (r < 0) { + // XXX: Handler errors are not propagated to the protocol layer. We may want to + // reconsider that + LOG(ERROR, "Connection handler failed: %d", r); + h->openFailed = true; + } + }); + state_ = State::OPEN; + if (pendingCloseError_) { + // close() was called from a connection handler + int error = pendingCloseError_; + pendingCloseError_ = 0; + close(error); // TODO: Call asynchronously + } +} + +void CoapChannel::close(int error) { + if (!error) { + error = SYSTEM_ERROR_COAP_CONNECTION_CLOSED; + } + openPending_ = false; + if (state_ != State::OPEN) { + if (state_ == State::OPENING) { + // close() is being called from a connection handler + pendingCloseError_ = error; + } + return; + } + state_ = State::CLOSING; + // Generate a new session ID to prevent the user code from messing up with the messages during + // the cleanup + ++sessId_; + releaseMessageBuffer(); + // Cancel device requests awaiting a response + forEachRefInList(sentReqs_, [=](auto req) { + if (req->type != MessageType::BLOCK_REQUEST && req->state == MessageState::WAIT_RESPONSE && req->errorCallback) { + req->errorCallback(error, req->id, req->callbackArg); // Callback passed to coap_write_payload() or coap_end_request() + } + req->state = MessageState::DONE; + // Release the unmanaged reference to the message + req->release(); + }); + sentReqs_ = nullptr; + // Cancel device requests and responses awaiting an ACK + forEachRefInList(unackMsgs_, [=](auto msg) { + if (msg->type != MessageType::BLOCK_REQUEST && msg->state == MessageState::WAIT_ACK && msg->errorCallback) { + msg->errorCallback(error, msg->requestId, msg->callbackArg); // Callback passed to coap_write_payload(), coap_end_request() or coap_end_response() + } + msg->state = MessageState::DONE; + msg->release(); + }); + unackMsgs_ = nullptr; + // Cancel transfer of server blockwise responses + forEachRefInList(blockResps_, [=](auto msg) { + assert(msg->state == MessageState::WAIT_BLOCK); + if (msg->errorCallback) { + msg->errorCallback(error, msg->requestId, msg->callbackArg); // Callback passed to coap_read_payload() + } + msg->state = MessageState::DONE; + msg->release(); + }); + blockResps_ = nullptr; + // Cancel server requests awaiting a response + forEachRefInList(recvReqs_, [](auto req) { + // No need to invoke any callbacks for these + req->state = MessageState::DONE; + req->release(); + }); + recvReqs_ = nullptr; + // Invoke connection handlers + forEachInList(connHandlers_, [=](auto h) { + if (!h->openFailed) { + assert(h->callback); + int r = h->callback(error, COAP_CONNECTION_CLOSED, h->callbackArg); + if (r < 0) { + LOG(ERROR, "Connection handler failed: %d", r); + } + } + h->openFailed = false; // Clear the failed state + }); + state_ = State::CLOSED; + if (openPending_) { + // open() was called from a connection handler + openPending_ = false; + open(); // TODO: Call asynchronously + } +} + +int CoapChannel::handleCon(const Message& msgBuf) { + if (curMsgId_) { + // TODO: Support asynchronous reading/writing to multiple message instances + LOG(WARN, "CoAP message buffer is already in use"); + // Contents of the buffer have already been overwritten at this point + releaseMessageBuffer(); + } + msgBuf_ = msgBuf; // Makes a shallow copy + CoapMessageDecoder d; + CHECK(d.decode((const char*)msgBuf_.buf(), msgBuf_.length())); + if (d.type() != CoapType::CON) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + int r = 0; + if (isCoapRequestCode(d.code())) { + r = CHECK(handleRequest(d)); + } else { + r = CHECK(handleResponse(d)); + } + return r; // 0 or Result::HANDLED +} + +int CoapChannel::handleAck(const Message& msgBuf) { + if (curMsgId_) { + // TODO: Support asynchronous reading/writing to multiple message instances + LOG(WARN, "CoAP message buffer is already in use"); + // Contents of the buffer have already been overwritten at this point + releaseMessageBuffer(); + } + msgBuf_ = msgBuf; // Makes a shallow copy + CoapMessageDecoder d; + CHECK(d.decode((const char*)msgBuf_.buf(), msgBuf_.length())); + if (d.type() != CoapType::ACK) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + return CHECK(handleAck(d)); +} + +int CoapChannel::handleRst(const Message& msgBuf) { + if (curMsgId_) { + // TODO: Support asynchronous reading/writing to multiple message instances + LOG(WARN, "CoAP message buffer is already in use"); + // Contents of the buffer have already been overwritten at this point + releaseMessageBuffer(); + } + msgBuf_ = msgBuf; // Makes a shallow copy + CoapMessageDecoder d; + CHECK(d.decode((const char*)msgBuf_.buf(), msgBuf_.length())); + if (d.type() != CoapType::RST) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + auto msg = findRefInList(unackMsgs_, [=](auto msg) { + return msg->coapId == d.id(); + }); + if (!msg) { + return 0; + } + assert(msg->state == MessageState::WAIT_ACK); + if (msg->type == MessageType::BLOCK_REQUEST) { + auto req = staticPtrCast(msg); + auto resp = RefCountPtr(req->blockResponse); // blockResponse is a raw pointer + assert(resp); + if (resp->errorCallback) { + resp->errorCallback(SYSTEM_ERROR_COAP_MESSAGE_RESET, resp->requestId, resp->callbackArg); // Callback passed to coap_read_response() + } + } else if (msg->errorCallback) { // REQUEST or RESPONSE + msg->errorCallback(SYSTEM_ERROR_COAP_MESSAGE_RESET, msg->requestId, msg->callbackArg); // Callback passed to coap_write_payload(), coap_end_request() or coap_end_response() + } + clearMessage(msg); + return Result::HANDLED; +} + +int CoapChannel::run() { + // TODO: ACK timeouts are handled by the old protocol implementation. As of now, the server always + // replies with piggybacked responses so we don't need to handle separate response timeouts either + return 0; +} + +CoapChannel* CoapChannel::instance() { + static CoapChannel channel; + return &channel; +} + +int CoapChannel::handleRequest(CoapMessageDecoder& d) { + if (d.tokenSize() != sizeof(token_t)) { // TODO: Support empty tokens + return 0; + } + // Get the request URI + char uri = '/'; // TODO: Support longer URIs + bool hasUri = false; + bool hasBlockOpt = false; + // TODO: Add a helper function for reconstructing the URI string from CoAP options + auto it = d.options(); + while (it.next()) { + if (it.option() == CoapOption::URI_PATH) { + if (it.size() > 1) { + return 0; // URI is too long, treat as an unrecognized request + } + if (it.size() > 0) { + if (hasUri) { + return 0; // ditto + } + uri = *it.data(); + hasUri = true; + } + } else if (it.option() == CoapOption::BLOCK1) { + hasBlockOpt = true; + } + } + // Find a request handler + auto method = d.code(); + auto h = findInList(reqHandlers_, [=](auto h) { + return h->uri == uri && h->method == method; + }); + if (!h) { + // The new CoAP API is implemented as an extension to the old protocol layer so, technically, + // the request may still be handled elsewhere + return 0; + } + if (hasBlockOpt) { + // TODO: Support cloud-to-device blockwise requests + LOG(WARN, "Received blockwise request"); + CHECK(sendAck(d.id(), true /* rst */)); + return Result::HANDLED; + } + // Acknowledge the request + assert(d.type() == CoapType::CON); // TODO: Support non-confirmable requests + CHECK(sendAck(d.id())); + // Create a message object + auto req = makeRefCountPtr(); + if (!req) { + return SYSTEM_ERROR_NO_MEMORY; + } + auto msgId = ++lastMsgId_; + req->id = msgId; + req->requestId = msgId; + req->sessionId = sessId_; + req->uri = uri; + req->method = static_cast(method); + req->coapId = d.id(); + assert(d.tokenSize() == sizeof(req->token)); + std::memcpy(&req->token, d.token(), d.tokenSize()); + req->pos = const_cast(d.payload()); + req->end = req->pos + d.payloadSize(); + req->state = MessageState::READ; + addRefToList(recvReqs_, req); + // Acquire the message buffer + assert(!curMsgId_); // Cleared in handleCon() + if (req->pos < req->end) { + curMsgId_ = msgId; + } + NAMED_SCOPE_GUARD(releaseMsgBufGuard, { + releaseMessageBuffer(); + }); + // Invoke the request handler + char uriStr[3] = { '/' }; + if (hasUri) { + uriStr[1] = uri; + } + assert(h->callback); + int r = h->callback(reinterpret_cast(req.get()), uriStr, req->method, req->id, h->callbackArg); + if (r < 0) { + LOG(ERROR, "Request handler failed: %d", r); + clearMessage(req); + return Result::HANDLED; + } + // Transfer ownership over the message to called code + req.unwrap(); + releaseMsgBufGuard.dismiss(); + return Result::HANDLED; +} + +int CoapChannel::handleResponse(CoapMessageDecoder& d) { + if (d.tokenSize() != sizeof(token_t)) { // TODO: Support empty tokens + return 0; + } + token_t token = 0; + std::memcpy(&token, d.token(), d.tokenSize()); + // Find the request which this response is meant for + auto req = findRefInList(sentReqs_, [=](auto req) { + return req->token == token; + }); + if (!req) { + int r = 0; + if (d.type() == CoapType::CON) { + // Check the unack'd requests as this response could arrive before the ACK. In that case, + // handleResponse() will be called recursively + r = CHECK(handleAck(d)); + } + return r; // 0 or Result::HANDLED + } + assert(req->state == MessageState::WAIT_RESPONSE); + removeRefFromList(sentReqs_, req); + req->state = MessageState::DONE; + if (d.type() == CoapType::CON) { + // Acknowledge the response + CHECK(sendAck(d.id())); + } + // Check if it's a blockwise response + auto resp = RefCountPtr(req->blockResponse); // blockResponse is a raw pointer. If null, a response object hasn't been created yet + const char* etag = nullptr; + size_t etagSize = 0; + int blockIndex = -1; + bool hasMore = false; + auto it = d.options(); + while (it.next()) { + int r = 0; + if (it.option() == CoapOption::BLOCK2) { + r = decodeBlockOption(it.toUInt(), blockIndex, hasMore); + } else if (it.option() == CoapOption::ETAG) { + etag = it.data(); + etagSize = it.size(); + if (etagSize > MAX_TAG_SIZE) { + r = SYSTEM_ERROR_COAP; + } + } + if (r < 0) { + LOG(ERROR, "Failed to decode message options: %d", r); + if (resp) { + if (resp->errorCallback) { + resp->errorCallback(SYSTEM_ERROR_COAP, resp->requestId, resp->callbackArg); + } + clearMessage(resp); + } + return Result::HANDLED; + } + } + if (req->type == MessageType::BLOCK_REQUEST) { + // Received another block of a blockwise response + assert(req->blockIndex.has_value() && resp); + if (blockIndex != req->blockIndex.value() || !etag || etagSize != req->tagSize || + std::memcmp(etag, req->tag, etagSize) != 0) { + auto code = d.code(); + LOG(ERROR, "Blockwise transfer failed: %d.%02d", (int)coapCodeClass(code), (int)coapCodeDetail(code)); + if (resp->errorCallback) { + resp->errorCallback(SYSTEM_ERROR_COAP, resp->requestId, resp->callbackArg); + } + clearMessage(resp); + return Result::HANDLED; + } + resp->blockIndex = blockIndex; + resp->hasMore = hasMore; + resp->pos = const_cast(d.payload()); + resp->end = resp->pos + d.payloadSize(); + assert(resp->state == MessageState::WAIT_BLOCK); + removeRefFromList(blockResps_, resp); + resp->state = MessageState::READ; + // Acquire the message buffer + assert(!curMsgId_); // Cleared in handleCon() + if (resp->pos < resp->end) { + curMsgId_ = resp->id; + } + NAMED_SCOPE_GUARD(releaseMsgBufGuard, { + releaseMessageBuffer(); + }); + // Invoke the block handler + assert(resp->blockCallback); + int r = resp->blockCallback(reinterpret_cast(resp.get()), resp->requestId, resp->callbackArg); + if (r < 0) { + LOG(ERROR, "Message block handler failed: %d", r); + clearMessage(resp); + return Result::HANDLED; + } + releaseMsgBufGuard.dismiss(); + return Result::HANDLED; + } + if (req->blockIndex.has_value() && req->hasMore.value()) { + // Received a response for a non-final block of a blockwise request + auto code = d.code(); + if (code == CoapCode::CONTINUE) { + req->state = MessageState::WRITE; + // Invoke the block handler + assert(req->blockCallback); + int r = req->blockCallback(reinterpret_cast(req.get()), req->id, req->callbackArg); + if (r < 0) { + LOG(ERROR, "Message block handler failed: %d", r); + clearMessage(req); + } + } else { + LOG(ERROR, "Blockwise transfer failed: %d.%02d", (int)coapCodeClass(code), (int)coapCodeDetail(code)); + if (req->errorCallback) { + req->errorCallback(SYSTEM_ERROR_COAP, req->id, req->callbackArg); // Callback passed to coap_write_payload() + } + clearMessage(req); + } + return Result::HANDLED; + } + // Received a regular response or the first block of a blockwise response + if (!req->responseCallback) { + return Result::HANDLED; // :shrug: + } + // Create a message object + resp = makeRefCountPtr(); + if (!resp) { + return SYSTEM_ERROR_NO_MEMORY; + } + resp->id = ++lastMsgId_; + resp->requestId = req->id; + resp->sessionId = sessId_; + resp->coapId = d.id(); + resp->token = token; + resp->status = d.code(); + resp->pos = const_cast(d.payload()); + resp->end = resp->pos + d.payloadSize(); + resp->state = MessageState::READ; + if (blockIndex >= 0) { + // This CoAP implementation requires the server to use a ETag option with all blockwise + // responses. The first block must have an index of 0 + if (blockIndex != 0 || !etagSize) { + LOG(ERROR, "Received invalid blockwise response"); + if (req->errorCallback) { + req->errorCallback(SYSTEM_ERROR_COAP, req->id, req->callbackArg); // Callback passed to coap_end_request() + } + return Result::HANDLED; + } + resp->blockIndex = blockIndex; + resp->hasMore = hasMore; + resp->blockRequest = req; + req->type = MessageType::BLOCK_REQUEST; + req->blockResponse = resp.get(); + req->blockIndex = resp->blockIndex; + req->hasMore = false; + req->tagSize = etagSize; + std::memcpy(req->tag, etag, etagSize); + } + // Acquire the message buffer + assert(!curMsgId_); + if (resp->pos < resp->end) { + curMsgId_ = resp->id; + } + NAMED_SCOPE_GUARD(releaseMsgBufGuard, { + releaseMessageBuffer(); + }); + // Invoke the response handler + int r = req->responseCallback(reinterpret_cast(resp.get()), resp->status, req->id, req->callbackArg); + if (r < 0) { + LOG(ERROR, "Response handler failed: %d", r); + clearMessage(resp); + return Result::HANDLED; + } + // Transfer ownership over the message to called code + resp.unwrap(); + releaseMsgBufGuard.dismiss(); + return Result::HANDLED; +} + +int CoapChannel::handleAck(CoapMessageDecoder& d) { + auto msg = findRefInList(unackMsgs_, [=](auto msg) { + return msg->coapId == d.id(); + }); + if (!msg) { + return 0; + } + assert(msg->state == MessageState::WAIT_ACK); + // For a blockwise request, the ACK callback is invoked when the last message block is acknowledged + if (msg->ackCallback && ((msg->type == MessageType::REQUEST && !msg->hasMore.value_or(false)) || + msg->type == MessageType::RESPONSE)) { + int r = msg->ackCallback(msg->requestId, msg->callbackArg); + if (r < 0) { + LOG(ERROR, "ACK handler failed: %d", r); + clearMessage(msg); + return Result::HANDLED; + } + } + if (msg->state == MessageState::WAIT_ACK) { + removeRefFromList(unackMsgs_, msg); + msg->state = MessageState::DONE; + if (msg->type == MessageType::REQUEST || msg->type == MessageType::BLOCK_REQUEST) { + msg->state = MessageState::WAIT_RESPONSE; + addRefToList(sentReqs_, staticPtrCast(msg)); + if (isCoapResponseCode(d.code())) { + CHECK(handleResponse(d)); + } + } + } + return Result::HANDLED; +} + +int CoapChannel::prepareMessage(const RefCountPtr& msg) { + assert(!curMsgId_); + CHECK_PROTOCOL(protocol_->get_channel().create(msgBuf_)); + if (msg->type == MessageType::REQUEST || msg->type == MessageType::BLOCK_REQUEST) { + msg->token = protocol_->get_next_token(); + } + msg->prefixSize = 0; + msg->pos = (char*)msgBuf_.buf(); + CHECK(updateMessage(msg)); + curMsgId_ = msg->id; + return 0; +} + +int CoapChannel::updateMessage(const RefCountPtr& msg) { + assert(curMsgId_ == msg->id); + char prefix[128]; + CoapMessageEncoder e(prefix, sizeof(prefix)); + e.type(CoapType::CON); + e.id(0); // Will be set by the underlying message channel + bool isRequest = msg->type == MessageType::REQUEST || msg->type == MessageType::BLOCK_REQUEST; + if (isRequest) { + auto req = staticPtrCast(msg); + e.code((int)req->method); + } else { + auto resp = staticPtrCast(msg); + e.code(resp->status); + } + e.token((const char*)&msg->token, sizeof(msg->token)); + // TODO: Support user-provided options + if (isRequest) { + auto req = staticPtrCast(msg); + if (req->type == MessageType::BLOCK_REQUEST && req->tagSize > 0) { + // Requesting the next block of a blockwise response + e.option(CoapOption::ETAG /* 4 */, req->tag, req->tagSize); + } + e.option(CoapOption::URI_PATH /* 11 */, &req->uri, 1); // TODO: Support longer URIs + if (req->blockIndex.has_value()) { + // See control vs descriptive usage of the block options in RFC 7959, 2.3 + if (req->type == MessageType::BLOCK_REQUEST) { + auto opt = encodeBlockOption(req->blockIndex.value(), false /* m */); + e.option(CoapOption::BLOCK2 /* 23 */, opt); + } else { + assert(req->hasMore.has_value()); + auto opt = encodeBlockOption(req->blockIndex.value(), req->hasMore.value()); + e.option(CoapOption::BLOCK1 /* 27 */, opt); + } + } + if (req->type == MessageType::REQUEST && req->tagSize > 0) { + // Sending the next block of a blockwise request + e.option(CoapOption::REQUEST_TAG /* 292 */, req->tag, req->tagSize); + } + } // TODO: Support device-to-cloud blockwise responses + auto msgBuf = (char*)msgBuf_.buf(); + size_t newPrefixSize = CHECK(e.encode()); + if (newPrefixSize > sizeof(prefix)) { + LOG(ERROR, "Too many CoAP options"); + return SYSTEM_ERROR_TOO_LARGE; + } + if (msg->prefixSize != newPrefixSize) { + size_t maxMsgSize = newPrefixSize + COAP_BLOCK_SIZE + 1; // Add 1 byte for a payload marker + if (maxMsgSize > msgBuf_.capacity()) { + LOG(ERROR, "No enough space in CoAP message buffer"); + return SYSTEM_ERROR_TOO_LARGE; + } + // Make room for the updated prefix data + size_t suffixSize = msg->pos - msgBuf - msg->prefixSize; // Size of the payload data with the payload marker + std::memmove(msgBuf + newPrefixSize, msgBuf + msg->prefixSize, suffixSize); + msg->pos += (int)newPrefixSize - (int)msg->prefixSize; + msg->end = msgBuf + maxMsgSize; + msg->prefixSize = newPrefixSize; + } + std::memcpy(msgBuf, prefix, msg->prefixSize); + return 0; +} + +int CoapChannel::sendMessage(RefCountPtr msg) { + assert(curMsgId_ == msg->id); + msgBuf_.set_length(msg->pos - (char*)msgBuf_.buf()); + CHECK_PROTOCOL(protocol_->get_channel().send(msgBuf_)); + msg->coapId = msgBuf_.get_id(); + msg->state = MessageState::WAIT_ACK; + msg->pos = nullptr; + addRefToList(unackMsgs_, std::move(msg)); + releaseMessageBuffer(); + return 0; +} + +int CoapChannel::sendAck(int coapId, bool rst) { + Message msg; + CHECK_PROTOCOL(protocol_->get_channel().response(msgBuf_, msg, msgBuf_.capacity() - msgBuf_.length())); + CoapMessageEncoder e((char*)msg.buf(), msg.capacity()); + e.type(rst ? CoapType::RST : CoapType::ACK); + e.code(CoapCode::EMPTY); + e.id(0); // Will be set by the underlying message channel + size_t n = CHECK(e.encode()); + if (n > msg.capacity()) { + LOG(ERROR, "No enough space in CoAP message buffer"); + return SYSTEM_ERROR_TOO_LARGE; + } + msg.set_length(n); + msg.set_id(coapId); + CHECK_PROTOCOL(protocol_->get_channel().send(msg)); + return 0; +} + +void CoapChannel::clearMessage(const RefCountPtr& msg) { + if (!msg || msg->sessionId != sessId_) { + return; + } + switch (msg->state) { + case MessageState::READ: { + if (msg->type == MessageType::REQUEST) { + removeRefFromList(recvReqs_, msg); + } + break; + } + case MessageState::WAIT_ACK: { + removeRefFromList(unackMsgs_, msg); + break; + } + case MessageState::WAIT_RESPONSE: { + assert(msg->type == MessageType::REQUEST); + removeRefFromList(sentReqs_, msg); + break; + } + case MessageState::WAIT_BLOCK: { + assert(msg->type == MessageType::RESPONSE); + auto resp = staticPtrCast(msg); + removeRefFromList(blockResps_, resp); + // Cancel the ongoing block request for this response + auto req = resp->blockRequest; + assert(req); + switch (req->state) { + case MessageState::WAIT_ACK: { + removeRefFromList(unackMsgs_, req); + break; + } + case MessageState::WAIT_RESPONSE: { + removeRefFromList(sentReqs_, req); + break; + } + default: + break; + } + req->state = MessageState::DONE; + break; + } + default: + break; + } + if (curMsgId_ == msg->id) { + releaseMessageBuffer(); + } + msg->state = MessageState::DONE; +} + +int CoapChannel::handleProtocolError(ProtocolError error) { + if (error == ProtocolError::NO_ERROR) { + return 0; + } + LOG(ERROR, "Protocol error: %d", (int)error); + int err = toSystemError(error); + close(err); + error = protocol_->get_channel().command(Channel::CLOSE); + if (error != ProtocolError::NO_ERROR) { + LOG(ERROR, "Channel CLOSE command failed: %d", (int)error); + } + return err; +} + +void CoapChannel::releaseMessageBuffer() { + msgBuf_.clear(); + curMsgId_ = 0; +} + +system_tick_t CoapChannel::millis() const { + return protocol_->get_callbacks().millis(); +} + +} // namespace particle::protocol::experimental + +using namespace particle::protocol::experimental; + +int coap_add_connection_handler(coap_connection_callback cb, void* arg, void* reserved) { + CHECK(CoapChannel::instance()->addConnectionHandler(cb, arg)); + return 0; +} + +void coap_remove_connection_handler(coap_connection_callback cb, void* reserved) { + CoapChannel::instance()->removeConnectionHandler(cb); +} + +int coap_add_request_handler(const char* uri, int method, int flags, coap_request_callback cb, void* arg, void* reserved) { + if (!isValidCoapMethod(method) || flags != 0) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + CHECK(CoapChannel::instance()->addRequestHandler(uri, static_cast(method), cb, arg)); + return 0; +} + +void coap_remove_request_handler(const char* uri, int method, void* reserved) { + if (!isValidCoapMethod(method)) { + return; + } + CoapChannel::instance()->removeRequestHandler(uri, static_cast(method)); +} + +int coap_begin_request(coap_message** msg, const char* uri, int method, int timeout, int flags, void* reserved) { + if (!isValidCoapMethod(method) || flags != 0) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + auto reqId = CHECK(CoapChannel::instance()->beginRequest(msg, uri, static_cast(method), timeout)); + return reqId; +} + +int coap_end_request(coap_message* msg, coap_response_callback resp_cb, coap_ack_callback ack_cb, + coap_error_callback error_cb, void* arg, void* reserved) { + CHECK(CoapChannel::instance()->endRequest(msg, resp_cb, ack_cb, error_cb, arg)); + return 0; +} + +int coap_begin_response(coap_message** msg, int status, int req_id, int flags, void* reserved) { + CHECK(CoapChannel::instance()->beginResponse(msg, status, req_id)); + return 0; +} + +int coap_end_response(coap_message* msg, coap_ack_callback ack_cb, coap_error_callback error_cb, + void* arg, void* reserved) { + CHECK(CoapChannel::instance()->endResponse(msg, ack_cb, error_cb, arg)); + return 0; +} + +void coap_destroy_message(coap_message* msg, void* reserved) { + CoapChannel::instance()->destroyMessage(msg); +} + +void coap_cancel_request(int req_id, void* reserved) { + CoapChannel::instance()->cancelRequest(req_id); +} + +int coap_write_payload(coap_message* msg, const char* data, size_t* size, coap_block_callback block_cb, + coap_error_callback error_cb, void* arg, void* reserved) { + int r = CHECK(CoapChannel::instance()->writePayload(msg, data, *size, block_cb, error_cb, arg)); + return r; // 0 or COAP_RESULT_WAIT_BLOCK +} + +int coap_read_payload(coap_message* msg, char* data, size_t* size, coap_block_callback block_cb, + coap_error_callback error_cb, void* arg, void* reserved) { + int r = CHECK(CoapChannel::instance()->readPayload(msg, data, *size, block_cb, error_cb, arg)); + return r; // 0 or COAP_RESULT_WAIT_BLOCK +} + +int coap_peek_payload(coap_message* msg, char* data, size_t size, void* reserved) { + size_t n = CHECK(CoapChannel::instance()->peekPayload(msg, data, size)); + return n; +} + +int coap_get_option(coap_option** opt, int num, coap_message* msg, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_get_next_option(coap_option** opt, int* num, coap_message* msg, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_get_uint_option_value(const coap_option* opt, unsigned* val, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_get_uint64_option_value(const coap_option* opt, uint64_t* val, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_get_string_option_value(const coap_option* opt, char* data, size_t size, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_get_opaque_option_value(const coap_option* opt, char* data, size_t size, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_add_empty_option(coap_message* msg, int num, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_add_uint_option(coap_message* msg, int num, unsigned val, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_add_uint64_option(coap_message* msg, int num, uint64_t val, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_add_string_option(coap_message* msg, int num, const char* val, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} + +int coap_add_opaque_option(coap_message* msg, int num, const char* data, size_t size, void* reserved) { + return SYSTEM_ERROR_NOT_SUPPORTED; // TODO +} diff --git a/communication/src/coap_defs.h b/communication/src/coap_defs.h index 7fbe7c3413..f3a35005e0 100644 --- a/communication/src/coap_defs.h +++ b/communication/src/coap_defs.h @@ -58,6 +58,7 @@ enum class CoapCode { VALID = coapCode(2, 3), CHANGED = coapCode(2, 4), CONTENT = coapCode(2, 5), + CONTINUE = coapCode(2, 31), // RFC 7959, 2.9. Response Codes BAD_REQUEST = coapCode(4, 0), UNAUTHORIZED = coapCode(4, 1), BAD_OPTION = coapCode(4, 2), @@ -65,6 +66,7 @@ enum class CoapCode { NOT_FOUND = coapCode(4, 4), METHOD_NOT_ALLOWED = coapCode(4, 5), NOT_ACCEPTABLE = coapCode(4, 6), + REQUEST_ENTITY_INCOMPLETE = coapCode(4, 8), // RFC 7959, 2.9. Response Codes PRECONDITION_FAILED = coapCode(4, 12), REQUEST_ENTITY_TOO_LARGE = coapCode(4, 13), UNSUPPORTED_CONTENT_FORMAT = coapCode(4, 15), @@ -98,7 +100,9 @@ enum class CoapOption { // RFC 7959, 2.1. The Block2 and Block1 Options; 4. The Size2 and Size1 Options BLOCK2 = 23, BLOCK1 = 27, - SIZE2 = 28 + SIZE2 = 28, + // RFC 9175, 3.2. The Request-Tag Option + REQUEST_TAG = 292 }; PARTICLE_DEFINE_ENUM_COMPARISON_OPERATORS(CoapOption) diff --git a/communication/src/dtls_message_channel.cpp b/communication/src/dtls_message_channel.cpp index 96802b3cae..a8c76a83b4 100644 --- a/communication/src/dtls_message_channel.cpp +++ b/communication/src/dtls_message_channel.cpp @@ -49,6 +49,7 @@ void mbedtls_ssl_update_out_pointers(mbedtls_ssl_context *ssl, mbedtls_ssl_trans #include #include "dtls_session_persist.h" #include "coap_channel.h" +#include "coap_channel_new.h" #include "coap_util.h" #include "platforms.h" @@ -290,6 +291,7 @@ inline int DTLSMessageChannel::send(const uint8_t* data, size_t len) void DTLSMessageChannel::reset_session() { + experimental::CoapChannel::instance()->close(); cancel_move_session(); mbedtls_ssl_session_reset(&ssl_context); sessionPersist.clear(callbacks.save); diff --git a/communication/src/protocol.cpp b/communication/src/protocol.cpp index 3f41a4c84d..8fd93cd426 100644 --- a/communication/src/protocol.cpp +++ b/communication/src/protocol.cpp @@ -31,6 +31,7 @@ LOG_SOURCE_CATEGORY("comm.protocol") #include "chunked_transfer.h" #include "subscriptions.h" #include "functions.h" +#include "coap_channel_new.h" #include "coap_message_decoder.h" #include "coap_message_encoder.h" @@ -214,8 +215,18 @@ ProtocolError Protocol::handle_received_message(Message& message, break; case CoAPMessageType::ERROR: - default: - ; // drop it on the floor + default: { + int r = 0; + if (type == CoAPType::CON) { + r = experimental::CoapChannel::instance()->handleCon(message); + } else if (type == CoAPType::ACK) { + r = experimental::CoapChannel::instance()->handleAck(message); + } + if (r < 0) { + return ProtocolError::COAP_ERROR; + } + break; + } } // all's well @@ -540,6 +551,7 @@ void Protocol::reset() { ack_handlers.clear(); channel.reset(); subscription_msg_ids.clear(); + experimental::CoapChannel::instance()->close(); } /** @@ -641,12 +653,12 @@ ProtocolError Protocol::event_loop(CoAPMessageType::Enum& message_type) if (error) { // bail if and only if there was an error + LOG(ERROR,"Event loop error %d", error); #if HAL_PLATFORM_OTA_PROTOCOL_V3 firmwareUpdate.reset(); #else chunkedTransfer.cancel(); #endif - LOG(ERROR,"Event loop error %d", error); return error; } return error; diff --git a/communication/src/protocol_defs.cpp b/communication/src/protocol_defs.cpp index 6706e5370f..bad232fc22 100644 --- a/communication/src/protocol_defs.cpp +++ b/communication/src/protocol_defs.cpp @@ -73,6 +73,8 @@ system_error_t toSystemError(ProtocolError error) { return SYSTEM_ERROR_OTA; case IO_ERROR_REMOTE_END_CLOSED: return SYSTEM_ERROR_END_OF_STREAM; + case COAP_ERROR: + return SYSTEM_ERROR_COAP; default: return SYSTEM_ERROR_PROTOCOL; // Generic protocol error } diff --git a/crypto/inc/mbedtls_config_default.h b/crypto/inc/mbedtls_config_default.h index e88946153f..79c28216d5 100644 --- a/crypto/inc/mbedtls_config_default.h +++ b/crypto/inc/mbedtls_config_default.h @@ -32,6 +32,9 @@ #include "platforms.h" +// For size_t +#include + #if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_DEPRECATE) #define _CRT_SECURE_NO_DEPRECATE 1 #endif diff --git a/hal/inc/hal_platform.h b/hal/inc/hal_platform.h index 362fa736b5..d5d94fd8a0 100644 --- a/hal/inc/hal_platform.h +++ b/hal/inc/hal_platform.h @@ -622,4 +622,8 @@ #define HAL_PLATFORM_AUTOMATIC_CONNECTION_MANAGEMENT (0) #endif // HAL_PLATFORM_AUTOMATIC_CONNECTION_MANAGEMENT +#ifndef HAL_PLATFORM_LEDGER +#define HAL_PLATFORM_LEDGER (1) +#endif // HAL_PLATFORM_LEDGER + #endif /* HAL_PLATFORM_H */ diff --git a/hal/network/ncp/cellular/cellular_network_manager.cpp b/hal/network/ncp/cellular/cellular_network_manager.cpp index 60756d09a0..62a14b7c0e 100644 --- a/hal/network/ncp/cellular/cellular_network_manager.cpp +++ b/hal/network/ncp/cellular/cellular_network_manager.cpp @@ -76,7 +76,7 @@ int loadConfig(CellularConfig* conf) { DecodedCString dExtApn(&pbConf.external_sim.apn); DecodedCString dExtUser(&pbConf.external_sim.user); DecodedCString dExtPwd(&pbConf.external_sim.password); - const int r = decodeMessageFromFile(&file, PB(CellularConfig_fields), &pbConf); + const int r = decodeProtobufFromFile(&file, PB(CellularConfig_fields), &pbConf); if (r < 0) { LOG(ERROR, "Unable to parse network settings"); *conf = CellularConfig(); // Using default settings @@ -168,7 +168,7 @@ int saveConfig(const CellularConfig& conf) { } } pbConf.active_sim = (PB_CELLULAR(SimType))conf.activeSim; - CHECK(encodeMessageToFile(&file, PB(CellularConfig_fields), &pbConf)); + CHECK(encodeProtobufToFile(&file, PB(CellularConfig_fields), &pbConf)); LOG(TRACE, "Updated file: %s", CONFIG_FILE); ok = true; return 0; diff --git a/hal/network/ncp/wifi/wifi_network_manager.cpp b/hal/network/ncp/wifi/wifi_network_manager.cpp index c54c60bf5e..857d1333fd 100644 --- a/hal/network/ncp/wifi/wifi_network_manager.cpp +++ b/hal/network/ncp/wifi/wifi_network_manager.cpp @@ -105,7 +105,7 @@ int loadConfig(Vector* networks) { } return true; }; - const int r = decodeMessageFromFile(&file, PB(WifiConfig_fields), &pbConf); + const int r = decodeProtobufFromFile(&file, PB(WifiConfig_fields), &pbConf); if (r < 0) { LOG(ERROR, "Unable to parse network settings"); networks->clear(); @@ -164,7 +164,7 @@ int saveConfig(const Vector& networks) { } return true; }; - CHECK(encodeMessageToFile(&file, PB(WifiConfig_fields), &pbConf)); + CHECK(encodeProtobufToFile(&file, PB(WifiConfig_fields), &pbConf)); LOG(TRACE, "Updated file: %s", CONFIG_FILE); ok = true; return 0; diff --git a/hal/shared/filesystem.h b/hal/shared/filesystem.h index 9b0970d69f..0cf0d008bd 100644 --- a/hal/shared/filesystem.h +++ b/hal/shared/filesystem.h @@ -81,7 +81,7 @@ int filesystem_to_system_error(int error); namespace particle { namespace fs { struct FsLock { - FsLock(filesystem_t* fs = filesystem_get_instance(FILESYSTEM_INSTANCE_DEFAULT, nullptr)) + explicit FsLock(filesystem_t* fs = filesystem_get_instance(FILESYSTEM_INSTANCE_DEFAULT, nullptr)) : fs_(fs) { lock(); } diff --git a/hal/src/gcc/hal_platform_config.h b/hal/src/gcc/hal_platform_config.h index 55cd21acf2..509a9939f1 100644 --- a/hal/src/gcc/hal_platform_config.h +++ b/hal/src/gcc/hal_platform_config.h @@ -65,3 +65,5 @@ #endif #define HAL_PLATFORM_FREERTOS (0) + +#define HAL_PLATFORM_LEDGER (0) diff --git a/hal/src/nRF52840/mbedtls/mbedtls_config_platform.h b/hal/src/nRF52840/mbedtls/mbedtls_config_platform.h index 6e1d9dee1c..9878bb2dd0 100644 --- a/hal/src/nRF52840/mbedtls/mbedtls_config_platform.h +++ b/hal/src/nRF52840/mbedtls/mbedtls_config_platform.h @@ -1415,6 +1415,16 @@ */ #define MBEDTLS_SSL_RAW_PUBLIC_KEY_SUPPORT +/** + * \def MBEDTLS_SSL_DISABLE_PARSE_CERTIFICATE + * + * When using MBEDTLS_SSL_RAW_PUBLIC_KEY_SUPPORT, disable + * support for parsing X509 certificates entirely. + */ +#if PLATFORM_ID == PLATFORM_TRACKER +#define MBEDTLS_SSL_DISABLE_PARSE_CERTIFICATE +#endif // PLATFORM_ID == PLATFORM_TRACKER + /** * \def MBEDTLS_ZLIB_SUPPORT * diff --git a/hal/src/newhal/hal_platform_config.h b/hal/src/newhal/hal_platform_config.h index 193722885d..631230076c 100644 --- a/hal/src/newhal/hal_platform_config.h +++ b/hal/src/newhal/hal_platform_config.h @@ -66,3 +66,5 @@ #endif #define HAL_PLATFORM_FREERTOS (0) + +#define HAL_PLATFORM_LEDGER (0) diff --git a/hal/src/rtl872x/concurrent_hal.cpp b/hal/src/rtl872x/concurrent_hal.cpp index 8ef804e57f..08425167da 100644 --- a/hal/src/rtl872x/concurrent_hal.cpp +++ b/hal/src/rtl872x/concurrent_hal.cpp @@ -434,6 +434,7 @@ int os_semaphore_give(os_semaphore_t semaphore, bool reserved) */ int os_timer_create(os_timer_t* timer, unsigned period, void (*callback)(os_timer_t timer), void* const timer_id, bool one_shot, void* reserved) { + // TODO: Return an error if the period is 0 (see https://www.freertos.org/FreeRTOS-timers-xTimerCreate.html) *timer = xTimerCreate((_CREATE_NAME_TYPE*)"", period, !one_shot, timer_id, reinterpret_cast(callback)); return *timer==NULL; } diff --git a/modules/argon/system-part1/module_system_part1_export.ld b/modules/argon/system-part1/module_system_part1_export.ld index 736aadb455..ca90d18c97 100644 --- a/modules/argon/system-part1/module_system_part1_export.ld +++ b/modules/argon/system-part1/module_system_part1_export.ld @@ -30,3 +30,4 @@ PROVIDE (link_dynalib_location_hal_posix_syscall = platform_system_part1_dyna PROVIDE (link_dynalib_location_hal_storage = platform_system_part1_dynalib_table_flash_start + 112); PROVIDE (link_dynalib_location_hal_watchdog = platform_system_part1_dynalib_table_flash_start + 116); PROVIDE (link_dynalib_location_system_asset_manager = platform_system_part1_dynalib_table_flash_start + 120); +PROVIDE (link_dynalib_location_system_ledger = platform_system_part1_dynalib_table_flash_start + 124); diff --git a/modules/b5som/system-part1/module_system_part1_export.ld b/modules/b5som/system-part1/module_system_part1_export.ld index d74ad590dd..94cd2f1df0 100644 --- a/modules/b5som/system-part1/module_system_part1_export.ld +++ b/modules/b5som/system-part1/module_system_part1_export.ld @@ -30,3 +30,4 @@ PROVIDE (link_dynalib_location_hal_posix_syscall = platform_system_part1_dyna PROVIDE (link_dynalib_location_hal_storage = platform_system_part1_dynalib_table_flash_start + 112); PROVIDE (link_dynalib_location_hal_watchdog = platform_system_part1_dynalib_table_flash_start + 116); PROVIDE (link_dynalib_location_system_asset_manager = platform_system_part1_dynalib_table_flash_start + 120); +PROVIDE (link_dynalib_location_system_ledger = platform_system_part1_dynalib_table_flash_start + 124); diff --git a/modules/boron/system-part1/module_system_part1_export.ld b/modules/boron/system-part1/module_system_part1_export.ld index d74ad590dd..94cd2f1df0 100644 --- a/modules/boron/system-part1/module_system_part1_export.ld +++ b/modules/boron/system-part1/module_system_part1_export.ld @@ -30,3 +30,4 @@ PROVIDE (link_dynalib_location_hal_posix_syscall = platform_system_part1_dyna PROVIDE (link_dynalib_location_hal_storage = platform_system_part1_dynalib_table_flash_start + 112); PROVIDE (link_dynalib_location_hal_watchdog = platform_system_part1_dynalib_table_flash_start + 116); PROVIDE (link_dynalib_location_system_asset_manager = platform_system_part1_dynalib_table_flash_start + 120); +PROVIDE (link_dynalib_location_system_ledger = platform_system_part1_dynalib_table_flash_start + 124); diff --git a/modules/msom/system-part1/module_system_part1_export.ld b/modules/msom/system-part1/module_system_part1_export.ld index a4bdb08ba0..276c81bf66 100644 --- a/modules/msom/system-part1/module_system_part1_export.ld +++ b/modules/msom/system-part1/module_system_part1_export.ld @@ -29,4 +29,5 @@ PROVIDE (link_dynalib_location_offset_hal_storage = 104); PROVIDE (link_dynalib_location_offset_hal_cellular = 108); PROVIDE (link_dynalib_location_offset_hal_watchdog = 112); PROVIDE (link_dynalib_location_offset_hal_backup_ram = 116); -PROVIDE (link_dynalib_location_offset_system_asset_manager = 120); \ No newline at end of file +PROVIDE (link_dynalib_location_offset_system_asset_manager = 120); +PROVIDE (link_dynalib_location_offset_system_ledger = 124); diff --git a/modules/shared/nRF52840/inc/system-part1/export_system.inc b/modules/shared/nRF52840/inc/system-part1/export_system.inc index 657c1392ee..45e0b117da 100644 --- a/modules/shared/nRF52840/inc/system-part1/export_system.inc +++ b/modules/shared/nRF52840/inc/system-part1/export_system.inc @@ -27,3 +27,4 @@ #include "system_dynalib_net.h" #include "system_dynalib_cloud.h" #include "system_dynalib_asset_manager.h" +#include "system_dynalib_ledger.h" diff --git a/modules/shared/nRF52840/inc/system-part1/module_system_part1.inc b/modules/shared/nRF52840/inc/system-part1/module_system_part1.inc index 28b2193781..4904d22bc5 100644 --- a/modules/shared/nRF52840/inc/system-part1/module_system_part1.inc +++ b/modules/shared/nRF52840/inc/system-part1/module_system_part1.inc @@ -59,6 +59,10 @@ DYNALIB_TABLE_EXTERN(hal_watchdog); DYNALIB_TABLE_EXTERN(system_asset_manager); +#if HAL_PLATFORM_LEDGER +DYNALIB_TABLE_EXTERN(system_ledger); +#endif + // strange that this is needed given that the entire block is scoped extern "C" // without it, the section name doesn't match *.system_part2_module as expected in the linker script extern "C" __attribute__((externally_visible)) const void* const system_part1_module[]; @@ -118,6 +122,9 @@ extern "C" __attribute__((externally_visible)) const void* const system_part1_mo , DYNALIB_TABLE_NAME(hal_watchdog) #endif // HAL_PLATFORM_HW_WATCHDOG , DYNALIB_TABLE_NAME(system_asset_manager) +#if HAL_PLATFORM_LEDGER + , DYNALIB_TABLE_NAME(system_ledger) +#endif }; #include "system_part1_loader.c" diff --git a/modules/shared/rtl872x/inc/system-part1/export_system.inc b/modules/shared/rtl872x/inc/system-part1/export_system.inc index 657c1392ee..45e0b117da 100644 --- a/modules/shared/rtl872x/inc/system-part1/export_system.inc +++ b/modules/shared/rtl872x/inc/system-part1/export_system.inc @@ -27,3 +27,4 @@ #include "system_dynalib_net.h" #include "system_dynalib_cloud.h" #include "system_dynalib_asset_manager.h" +#include "system_dynalib_ledger.h" diff --git a/modules/shared/rtl872x/inc/system-part1/module_system_part1.inc b/modules/shared/rtl872x/inc/system-part1/module_system_part1.inc index 659a78034a..0f7450f774 100644 --- a/modules/shared/rtl872x/inc/system-part1/module_system_part1.inc +++ b/modules/shared/rtl872x/inc/system-part1/module_system_part1.inc @@ -50,6 +50,9 @@ DYNALIB_TABLE_EXTERN(hal_watchdog); #endif // HAL_PLATFORM_HW_WATCHDOG DYNALIB_TABLE_EXTERN(hal_backup_ram); DYNALIB_TABLE_EXTERN(system_asset_manager); +#if HAL_PLATFORM_LEDGER +DYNALIB_TABLE_EXTERN(system_ledger); +#endif // strange that this is needed given that the entire block is scoped extern "C" // without it, the section name doesn't match *.system_part2_module as expected in the linker script @@ -97,8 +100,11 @@ extern "C" __attribute__((externally_visible)) const void* const system_part1_mo #if HAL_PLATFORM_HW_WATCHDOG , DYNALIB_TABLE_NAME(hal_watchdog) #endif // HAL_PLATFORM_HW_WATCHDOG - , DYNALIB_TABLE_NAME(hal_backup_ram), - DYNALIB_TABLE_NAME(system_asset_manager) + , DYNALIB_TABLE_NAME(hal_backup_ram) + , DYNALIB_TABLE_NAME(system_asset_manager) +#if HAL_PLATFORM_LEDGER + , DYNALIB_TABLE_NAME(system_ledger) +#endif }; #include "system_part1_loader.c" diff --git a/modules/tracker/system-part1/module_system_part1_export.ld b/modules/tracker/system-part1/module_system_part1_export.ld index b073c60180..e3165fddfb 100644 --- a/modules/tracker/system-part1/module_system_part1_export.ld +++ b/modules/tracker/system-part1/module_system_part1_export.ld @@ -31,3 +31,4 @@ PROVIDE (link_dynalib_location_hal_wlan = platform_system_part1_dyna PROVIDE (link_dynalib_location_hal_storage = platform_system_part1_dynalib_table_flash_start + 116); PROVIDE (link_dynalib_location_hal_watchdog = platform_system_part1_dynalib_table_flash_start + 120); PROVIDE (link_dynalib_location_system_asset_manager = platform_system_part1_dynalib_table_flash_start + 124); +PROVIDE (link_dynalib_location_system_ledger = platform_system_part1_dynalib_table_flash_start + 128); diff --git a/modules/trackerm/system-part1/module_system_part1_export.ld b/modules/trackerm/system-part1/module_system_part1_export.ld index f1d18afa4b..276c81bf66 100644 --- a/modules/trackerm/system-part1/module_system_part1_export.ld +++ b/modules/trackerm/system-part1/module_system_part1_export.ld @@ -30,3 +30,4 @@ PROVIDE (link_dynalib_location_offset_hal_cellular = 108); PROVIDE (link_dynalib_location_offset_hal_watchdog = 112); PROVIDE (link_dynalib_location_offset_hal_backup_ram = 116); PROVIDE (link_dynalib_location_offset_system_asset_manager = 120); +PROVIDE (link_dynalib_location_offset_system_ledger = 124); diff --git a/modules/tron/system-part1/module_system_part1_export.ld b/modules/tron/system-part1/module_system_part1_export.ld index b5cf86753d..1634b555fc 100644 --- a/modules/tron/system-part1/module_system_part1_export.ld +++ b/modules/tron/system-part1/module_system_part1_export.ld @@ -28,4 +28,5 @@ PROVIDE (link_dynalib_location_offset_hal_posix_syscall = 100); PROVIDE (link_dynalib_location_offset_hal_storage = 104); PROVIDE (link_dynalib_location_offset_hal_watchdog = 108); PROVIDE (link_dynalib_location_offset_hal_backup_ram = 112); -PROVIDE (link_dynalib_location_offset_system_asset_manager = 116); \ No newline at end of file +PROVIDE (link_dynalib_location_offset_system_asset_manager = 116); +PROVIDE (link_dynalib_location_offset_system_ledger = 120); diff --git a/proto_defs/.gitignore b/proto_defs/.gitignore new file mode 100644 index 0000000000..ef81b1e243 --- /dev/null +++ b/proto_defs/.gitignore @@ -0,0 +1 @@ +/.venv/ diff --git a/proto_defs/gen_proto.sh b/proto_defs/gen_proto.sh index e95b747b7c..37a7cba0e5 100755 --- a/proto_defs/gen_proto.sh +++ b/proto_defs/gen_proto.sh @@ -1,17 +1,5 @@ #!/bin/bash -# Note for macOS users: -# -# 1. The following dependencies are required to build the protocol files using the nanopb plugin: -# -# brew install protobuf python -# pip2 install protobuf -# -# 2. Make sure your system Python can find Python modules installed via Homebrew: -# -# mkdir -p ~/Library/Python/2.7/lib/python/site-packages -# echo 'import site; site.addsitedir("/usr/local/lib/python2.7/site-packages")' >> ~/Library/Python/2.7/lib/python/site-packages/homebrew.pth - set -e DEVICE_OS_DIR="$(cd "$(dirname "$0")/.." && pwd)" @@ -32,7 +20,14 @@ gen_proto() { --nanopb_out="${DEST_DIR}" "$1" } -# Control requests +# Create a virtual environment +python3 -m venv "$PROTO_DEFS_DIR/.venv" +source "$PROTO_DEFS_DIR/.venv/bin/activate" + +# Install dependencies +pip3 install protobuf + +# Compile control request definitions gen_proto "${SHARED_DIR}/control/extensions.proto" gen_proto "${SHARED_DIR}/control/common.proto" gen_proto "${SHARED_DIR}/control/config.proto" @@ -44,10 +39,11 @@ gen_proto "${SHARED_DIR}/control/network_old.proto" gen_proto "${SHARED_DIR}/control/storage.proto" gen_proto "${SHARED_DIR}/control/cloud.proto" -# Cloud protocol +# Compile cloud protocol definitions gen_proto "${SHARED_DIR}/cloud/cloud.proto" gen_proto "${SHARED_DIR}/cloud/describe.proto" gen_proto "${SHARED_DIR}/cloud/ledger.proto" -# Internal definitions +# Compile internal definitions gen_proto "${INTERNAL_DIR}/network_config.proto" +gen_proto "${INTERNAL_DIR}/ledger.proto" diff --git a/proto_defs/internal/ledger.proto b/proto_defs/internal/ledger.proto new file mode 100644 index 0000000000..847f7c581a --- /dev/null +++ b/proto_defs/internal/ledger.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; + +package particle.firmware; + +import "cloud/ledger.proto"; +import "nanopb.proto"; + +/** + * Ledger info. + */ +message LedgerInfo { + string name = 1 [(nanopb).max_length = 32]; ///< Ledger name. + bytes scope_id = 2 [(nanopb).max_size = 32]; ///< Scope ID. + cloud.ledger.ScopeType scope_type = 3; ///< Scope type. + cloud.ledger.SyncDirection sync_direction = 4; ///< Sync direction. + /** + * Time the ledger was last updated, in milliseconds since the Unix epoch. + * + * If not set, the time is unknown. + */ + optional fixed64 last_updated = 5; + /** + * Time the ledger was last synchronized with the Cloud, in milliseconds since the Unix epoch. + * + * If not set, the ledger has never been synchronized. + */ + optional fixed64 last_synced = 6; + uint32 update_count = 7; ///< Counter incremented every time the ledger is updated. + bool sync_pending = 8; ///< Whether the ledger needs to be synchronized. +} diff --git a/proto_defs/src/ledger.pb.c b/proto_defs/src/ledger.pb.c new file mode 100644 index 0000000000..b1369806cb --- /dev/null +++ b/proto_defs/src/ledger.pb.c @@ -0,0 +1,12 @@ +/* Automatically generated nanopb constant definitions */ +/* Generated by nanopb-0.4.5 */ + +#include "ledger.pb.h" +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +PB_BIND(particle_firmware_LedgerInfo, particle_firmware_LedgerInfo, AUTO) + + + diff --git a/proto_defs/src/ledger.pb.h b/proto_defs/src/ledger.pb.h new file mode 100644 index 0000000000..5082a55f8b --- /dev/null +++ b/proto_defs/src/ledger.pb.h @@ -0,0 +1,82 @@ +/* Automatically generated nanopb header */ +/* Generated by nanopb-0.4.5 */ + +#ifndef PB_PARTICLE_FIRMWARE_LEDGER_PB_H_INCLUDED +#define PB_PARTICLE_FIRMWARE_LEDGER_PB_H_INCLUDED +#include +#include "cloud/ledger.pb.h" + +#if PB_PROTO_HEADER_VERSION != 40 +#error Regenerate this file with the current version of nanopb generator. +#endif + +/* Struct definitions */ +typedef PB_BYTES_ARRAY_T(32) particle_firmware_LedgerInfo_scope_id_t; +/* * + Ledger info. */ +typedef struct _particle_firmware_LedgerInfo { + char name[33]; /* /< Ledger name. */ + particle_firmware_LedgerInfo_scope_id_t scope_id; /* /< Scope ID. */ + particle_cloud_ledger_ScopeType scope_type; /* /< Scope type. */ + particle_cloud_ledger_SyncDirection sync_direction; /* /< Sync direction. */ + /* * + Time the ledger was last updated, in milliseconds since the Unix epoch. + + If not set, the time is unknown. */ + bool has_last_updated; + uint64_t last_updated; + /* * + Time the ledger was last synchronized with the Cloud, in milliseconds since the Unix epoch. + + If not set, the ledger has never been synchronized. */ + bool has_last_synced; + uint64_t last_synced; + uint32_t update_count; /* /< Counter incremented every time the ledger is updated. */ + bool sync_pending; /* /< Whether the ledger needs to be synchronized. */ +} particle_firmware_LedgerInfo; + + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initializer values for message structs */ +#define particle_firmware_LedgerInfo_init_default {"", {0, {0}}, _particle_cloud_ledger_ScopeType_MIN, _particle_cloud_ledger_SyncDirection_MIN, false, 0, false, 0, 0, 0} +#define particle_firmware_LedgerInfo_init_zero {"", {0, {0}}, _particle_cloud_ledger_ScopeType_MIN, _particle_cloud_ledger_SyncDirection_MIN, false, 0, false, 0, 0, 0} + +/* Field tags (for use in manual encoding/decoding) */ +#define particle_firmware_LedgerInfo_name_tag 1 +#define particle_firmware_LedgerInfo_scope_id_tag 2 +#define particle_firmware_LedgerInfo_scope_type_tag 3 +#define particle_firmware_LedgerInfo_sync_direction_tag 4 +#define particle_firmware_LedgerInfo_last_updated_tag 5 +#define particle_firmware_LedgerInfo_last_synced_tag 6 +#define particle_firmware_LedgerInfo_update_count_tag 7 +#define particle_firmware_LedgerInfo_sync_pending_tag 8 + +/* Struct field encoding specification for nanopb */ +#define particle_firmware_LedgerInfo_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, name, 1) \ +X(a, STATIC, SINGULAR, BYTES, scope_id, 2) \ +X(a, STATIC, SINGULAR, UENUM, scope_type, 3) \ +X(a, STATIC, SINGULAR, UENUM, sync_direction, 4) \ +X(a, STATIC, OPTIONAL, FIXED64, last_updated, 5) \ +X(a, STATIC, OPTIONAL, FIXED64, last_synced, 6) \ +X(a, STATIC, SINGULAR, UINT32, update_count, 7) \ +X(a, STATIC, SINGULAR, BOOL, sync_pending, 8) +#define particle_firmware_LedgerInfo_CALLBACK NULL +#define particle_firmware_LedgerInfo_DEFAULT NULL + +extern const pb_msgdesc_t particle_firmware_LedgerInfo_msg; + +/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ +#define particle_firmware_LedgerInfo_fields &particle_firmware_LedgerInfo_msg + +/* Maximum encoded size of messages (where known) */ +#define particle_firmware_LedgerInfo_size 98 + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/services/inc/debug_util.h b/services/inc/debug_util.h new file mode 100644 index 0000000000..c0731a7adc --- /dev/null +++ b/services/inc/debug_util.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include + +#include "hal_platform.h" + +/** + * Watchpoint type. + */ +typedef enum watchpoint_type { + WATCHPOINT_TYPE_WRITE = 0, ///< Write access. + WATCHPOINT_TYPE_READ = 1, ///< Read access. + WATCHPOINT_TYPE_READ_WRITE = 2 ///< Read/write access. +} watchpoint_type; + +#ifdef __cplusplus +extern "C" { +#endif + +#if HAL_PLATFORM_NRF52840 + +/** + * Set a watchpoint. + * + * @param addr Region address. Must be aligned by `size`. + * @param size Region size. Must be a power of 2. + * @param type Watchpoint type as defined by the `watchpoint_type` enum. + * @returns Index of the newly set watchpoint or an error code defined by the `system_error_t` enum. + */ +int set_watchpoint(const void* addr, size_t size, int type); // TODO: Inline or export via a dynalib + +/** + * Clear a watchpoint. + * + * @param idx Watchpoint index. + */ +void clear_watchpoint(int idx); + +/** + * Generate a breakpoint exception. + */ +void breakpoint(); + +#endif // HAL_PLATFORM_NRF52840 + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/services/inc/file_util.h b/services/inc/file_util.h index fc5995192f..c2bcd8f42d 100644 --- a/services/inc/file_util.h +++ b/services/inc/file_util.h @@ -26,8 +26,8 @@ namespace particle { int openFile(lfs_file_t* file, const char* path, unsigned flags = LFS_O_RDWR); int dumpFile(const char* path); -int decodeMessageFromFile(lfs_file_t* file, const pb_msgdesc_t* desc, void* msg); -int encodeMessageToFile(lfs_file_t* file, const pb_msgdesc_t* desc, const void* msg); +int decodeProtobufFromFile(lfs_file_t* file, const pb_msgdesc_t* desc, void* msg, int size = -1); +int encodeProtobufToFile(lfs_file_t* file, const pb_msgdesc_t* desc, const void* msg); int rmrf(const char* path); int mkdirp(const char* path); diff --git a/services/inc/nanopb_misc.h b/services/inc/nanopb_misc.h index 9be397a7f7..8ddd2ee035 100644 --- a/services/inc/nanopb_misc.h +++ b/services/inc/nanopb_misc.h @@ -28,6 +28,7 @@ extern "C" { #endif // __cplusplus typedef struct lfs_file lfs_file_t; +typedef struct coap_message coap_message; pb_ostream_t* pb_ostream_init(void* reserved); bool pb_ostream_free(pb_ostream_t* stream, void* reserved); @@ -39,10 +40,14 @@ bool pb_ostream_from_buffer_ex(pb_ostream_t* stream, pb_byte_t *buf, size_t bufs bool pb_istream_from_buffer_ex(pb_istream_t* stream, const pb_byte_t *buf, size_t bufsize, void* reserved); #if HAL_PLATFORM_FILESYSTEM -bool pb_ostream_from_file(pb_ostream_t* stream, lfs_file_t* file, void* reserved); -bool pb_istream_from_file(pb_istream_t* stream, lfs_file_t* file, void* reserved); +int pb_ostream_from_file(pb_ostream_t* stream, lfs_file_t* file, void* reserved); +int pb_istream_from_file(pb_istream_t* stream, lfs_file_t* file, int size, void* reserved); #endif // HAL_PLATFORM_FILESYSTEM +// These functions can only be used if the payload data fits in one CoAP message +int pb_istream_from_coap_message(pb_istream_t* stream, coap_message* msg, void* reserved); +int pb_ostream_from_coap_message(pb_ostream_t* stream, coap_message* msg, void* reserved); + #ifdef SERVICES_NO_NANOPB_LIB #pragma weak pb_ostream_init #pragma weak pb_ostream_free diff --git a/services/inc/preprocessor.h b/services/inc/preprocessor.h index c7fb220275..5c4a56f0c6 100644 --- a/services/inc/preprocessor.h +++ b/services/inc/preprocessor.h @@ -69,7 +69,7 @@ /* PP_COUNT(...) - Expands to a number of arguments. This macro supports up to 98 arguments. + Expands to a number of arguments. PP_COUNT(a, b) // Expands to 2 */ @@ -89,11 +89,11 @@ _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, \ _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, \ _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, \ - _91, _92, _93, _94, _95, _96, _97, _98, \ + _91, _92, _93, _94, _95, _96, _97, _98, _99, \ n, ...) n #define _PP_COUNT_N \ - 98, 97, 96, 95, 94, 93, 92, 91, \ + 99, 98, 97, 96, 95, 94, 93, 92, 91, \ 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, \ 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, \ 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, \ @@ -107,7 +107,7 @@ /* PP_FOR_EACH(macro, data, ...) - Expands macro for each argument. This macro supports up to 30 arguments. + Expands macro for each argument. #define CALL(func, arg) func(arg); PP_FOR_EACH(CALL, foo, 1, 2, 3) // Expands to foo(1); foo(2); foo(3); @@ -320,5 +320,7 @@ _PP_FOR_EACH_97(m, d, _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, _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, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, _91, _92, _93, _94, _95, _96, _97) m(d, _98) #define _PP_FOR_EACH_99(m, d, _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, _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, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, _91, _92, _93, _94, _95, _96, _97, _98, _99) \ _PP_FOR_EACH_98(m, d, _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, _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, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, _91, _92, _93, _94, _95, _96, _97, _98) m(d, _99) +#define _PP_FOR_EACH_100(m, d, _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, _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, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, _91, _92, _93, _94, _95, _96, _97, _98, _99, _100) \ + _PP_FOR_EACH_99(m, d, _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, _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, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _70, _71, _72, _73, _74, _75, _76, _77, _78, _79, _80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _90, _91, _92, _93, _94, _95, _96, _97, _98) m(d, _100) #endif // _PREPROCESSOR_H diff --git a/services/inc/ref_count.h b/services/inc/ref_count.h new file mode 100644 index 0000000000..7e2b158273 --- /dev/null +++ b/services/inc/ref_count.h @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include +#include +#include + +static_assert(std::atomic_int::is_always_lock_free, "std::atomic is not always lock-free"); + +namespace particle { + +/** + * Base class for reference counted objects. + */ +class RefCount { +public: + RefCount() : + count_(1) { + } + + RefCount(const RefCount&) = delete; + + virtual ~RefCount() = default; + + void addRef() const { + count_.fetch_add(1, std::memory_order_relaxed); + } + + void release() const { + if (count_.fetch_sub(1, std::memory_order_acq_rel) == 1) { + delete this; + } + } + + RefCount& operator=(const RefCount&) = delete; + +private: + mutable std::atomic_int count_; +}; + +/** + * Smart pointer for reference counted objects. + */ +template +class RefCountPtr { +public: + RefCountPtr() : + RefCountPtr(nullptr) { + } + + RefCountPtr(T* ptr) : + RefCountPtr(ptr, true /* addRef */) { + } + + RefCountPtr(const RefCountPtr& ptr) : + RefCountPtr(ptr.p_) { + } + + RefCountPtr(RefCountPtr&& ptr) : + RefCountPtr() { + swap(*this, ptr); + } + + ~RefCountPtr() { + if (p_) { + p_->release(); + } + } + + T* get() const { + return p_; + } + + T* unwrap() { + auto p = p_; + p_ = nullptr; + return p; + } + + T* operator->() const { + return p_; + } + + T& operator*() const { + return *p_; + } + + RefCountPtr& operator=(RefCountPtr ptr) { + swap(*this, ptr); + return *this; + } + + template>> + operator RefCountPtr() const { + return RefCountPtr(p_); + } + + explicit operator bool() const { + return p_; + } + + static RefCountPtr wrap(T* ptr) { + return RefCountPtr(ptr, false /* addRef */); + } + + friend void swap(RefCountPtr& ptr1, RefCountPtr& ptr2) { + using std::swap; + swap(ptr1.p_, ptr2.p_); + } + +private: + T* p_; + + RefCountPtr(T* p, bool addRef) : + p_(p) { + if (p_ && addRef) { + p_->addRef(); + } + } +}; + +template +inline RefCountPtr makeRefCountPtr(ArgsT&&... args) { + return RefCountPtr::wrap(new(std::nothrow) T(std::forward(args)...)); +} + +template +inline RefCountPtr staticPtrCast(const RefCountPtr& ptr) { + return static_cast(ptr.get()); +} + +} // namespace particle diff --git a/services/inc/system_error.h b/services/inc/system_error.h index 5f3b9252c1..b6e71be4bf 100644 --- a/services/inc/system_error.h +++ b/services/inc/system_error.h @@ -49,9 +49,15 @@ (NO_MEMORY, "Memory allocation error", -260), \ (INVALID_ARGUMENT, "Invalid argument", -270), \ (BAD_DATA, "Invalid data format", -280), \ + (ENCODING_FAILED, "Encoding error", -281), \ (OUT_OF_RANGE, "Out of range", -290), \ (DEPRECATED, "Deprecated", -300), \ (COAP, "CoAP error", -1000), /* -1199 ... -1000: CoAP errors */ \ + (COAP_CONNECTION_CLOSED, "Connection closed", -1001), \ + (COAP_MESSAGE_RESET, "Received a RST message", -1002), \ + (COAP_TIMEOUT, "CoAP timeout", -1003), \ + (COAP_REQUEST_NOT_FOUND, "Request not found", -1004), \ + (COAP_REQUEST_CANCELLED, "Request was cancelled", -1005), \ (COAP_4XX, "CoAP: 4xx", -1100), \ (COAP_5XX, "CoAP: 5xx", -1132), \ (AT_NOT_OK, "AT command failure", -1200), /* -1299 ... -1200: AT command errors */ \ @@ -102,7 +108,20 @@ (FILESYSTEM_FBIG, "File too large", -1909), \ (FILESYSTEM_INVAL, "Invalid parameter", -1910), \ (FILESYSTEM_NOSPC, "No space left in the filesystem", -1911), \ - (FILESYSTEM_NOMEM, "Memory allocation error", -1912) + (FILESYSTEM_NOMEM, "Memory allocation error", -1912), \ + (LEDGER_NOT_FOUND, "Ledger not found", -2000), /* -2099 ... -2000: Ledger errors */ \ + (LEDGER_INVALID_FORMAT, "Invalid format of ledger data", -2001), \ + (LEDGER_UNSUPPORTED_FORMAT, "Unsupported format of ledger data", -2002), \ + (LEDGER_READ_ONLY, "Ledger is read only", -2003), \ + (LEDGER_IN_USE, "Ledger is in use", -2004), \ + (LEDGER_TOO_LARGE, "Ledger data is too large", -2005), \ + (LEDGER_TOO_MANY, "Too many ledgers", -2006), \ + (LEDGER_INCONSISTENT_STATE, "Inconsistent ledger state", -2007), \ + (LEDGER_ENCODING_FAILED, "Ledger encoding error", -2008), \ + (LEDGER_DECODING_FAILED, "Ledger decoding error", -2009), \ + (LEDGER_REQUEST_FAILED, "Ledger request failed", -2010), \ + (LEDGER_INVALID_RESPONSE, "Invalid response from server", -2011), \ + (HAL_RTC_INVALID_TIME, "RTC time is invalid", -3000) /* -3099 ... -3000: HAL errors */ // Expands to enum values for all errors #define SYSTEM_ERROR_ENUM_VALUES(prefix) \ diff --git a/services/inc/time_util.h b/services/inc/time_util.h new file mode 100644 index 0000000000..9c508eb700 --- /dev/null +++ b/services/inc/time_util.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#include + +#include "rtc_hal.h" + +#include "system_error.h" + +namespace particle { + +/** + * Get the number of milliseconds elapsed since the Unix epoch. + * + * @return Number of milliseconds on success, otherwise an error code defined by `system_error_t`. + */ +inline int64_t getMillisSinceEpoch() { + if (!hal_rtc_time_is_valid(nullptr)) { + return SYSTEM_ERROR_HAL_RTC_INVALID_TIME; + } + timeval tv = {}; + int r = hal_rtc_get_time(&tv, nullptr); + if (r < 0) { + return r; + } + return tv.tv_sec * 1000ll + tv.tv_usec / 1000; +} + +} // namespace particle diff --git a/services/src/debug_util.cpp b/services/src/debug_util.cpp new file mode 100644 index 0000000000..75727cc70e --- /dev/null +++ b/services/src/debug_util.cpp @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#include + +#include "debug_util.h" + +#include "system_error.h" +#include "logging.h" + +#if HAL_PLATFORM_NRF52840 + +#include + +int set_watchpoint(const void* addr, size_t size, int type) { + size_t numComp = (DWT->CTRL >> 28) & 0x0f; + if (!numComp) { + LOG(ERROR, "Watchpoints are not supported"); + return SYSTEM_ERROR_NOT_SUPPORTED; + } + // TODO: Support more than one watchpoint + if (DWT->FUNCTION0) { + LOG(ERROR, "Watchpoint is already set"); + return SYSTEM_ERROR_ALREADY_EXISTS; + } + if (!size || (size & (size - 1)) != 0) { + LOG(ERROR, "Size is not a power of 2"); + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + // Determine the mask size + DWT->MASK0 = 0x1f; + uint32_t maxMaskBits = DWT->MASK0; + uint32_t maskBits = 0; + while (size & 1) { + ++maskBits; + size >>= 1; + } + if (maskBits > maxMaskBits) { + LOG(ERROR, "Size is too large"); + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + // Address must be aligned by size + auto addrVal = (uintptr_t)addr; + if ((addrVal & ((1 << maskBits) - 1)) != 0) { + LOG(ERROR, "Address is not aligned"); + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + uint32_t func = 0; + switch (type) { + case WATCHPOINT_TYPE_READ: + func = 0x05; + break; + case WATCHPOINT_TYPE_WRITE: + func = 0x06; + break; + case WATCHPOINT_TYPE_READ_WRITE: + func = 0x07; + break; + default: + LOG(ERROR, "Invalid watchpoint type"); + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + DWT->COMP0 = addrVal; + DWT->MASK0 = maskBits; + DWT->FUNCTION0 = func; // Enables the watchpoint + return 0; +} + +void clear_watchpoint(int idx) { + // TODO: Support more than one watchpoint + if (idx == 0) { + DWT->FUNCTION0 = 0; + } +} + +void breakpoint() { + asm("BKPT"); +} + +#endif // HAL_PLATFORM_NRF52840 diff --git a/services/src/file_util.cpp b/services/src/file_util.cpp index 8e64636c9f..7d7a6c76cf 100644 --- a/services/src/file_util.cpp +++ b/services/src/file_util.cpp @@ -166,26 +166,29 @@ int dumpFile(const char* path) { return 0; } -int decodeMessageFromFile(lfs_file_t* file, const pb_msgdesc_t* desc, void* msg) { +int decodeProtobufFromFile(lfs_file_t* file, const pb_msgdesc_t* desc, void* msg, int size) { + // TODO: nanopb is no longer exported as a dynalib so there's no need for allocating its objects + // on the heap const auto strm = pb_istream_init(nullptr); CHECK_TRUE(strm, SYSTEM_ERROR_NO_MEMORY); SCOPE_GUARD({ pb_istream_free(strm, nullptr); }); - CHECK_TRUE(pb_istream_from_file(strm, file, nullptr), SYSTEM_ERROR_FILE); + CHECK(pb_istream_from_file(strm, file, size, nullptr)); + size = strm->bytes_left; CHECK_TRUE(pb_decode(strm, desc, msg), SYSTEM_ERROR_FILE); - return 0; + return size; } -int encodeMessageToFile(lfs_file_t* file, const pb_msgdesc_t* desc, const void* msg) { +int encodeProtobufToFile(lfs_file_t* file, const pb_msgdesc_t* desc, const void* msg) { const auto strm = pb_ostream_init(nullptr); CHECK_TRUE(strm, SYSTEM_ERROR_NO_MEMORY); SCOPE_GUARD({ pb_ostream_free(strm, nullptr); }); - CHECK_TRUE(pb_ostream_from_file(strm, file, nullptr), SYSTEM_ERROR_FILE); + CHECK(pb_ostream_from_file(strm, file, nullptr)); CHECK_TRUE(pb_encode(strm, desc, msg), SYSTEM_ERROR_FILE); - return 0; + return strm->bytes_written; } int rmrf(const char* path) { diff --git a/services/src/nanopb_misc.c b/services/src/nanopb_misc.c index 092e5f9183..4d503b7f1a 100644 --- a/services/src/nanopb_misc.c +++ b/services/src/nanopb_misc.c @@ -17,7 +17,12 @@ #include "nanopb_misc.h" +// FIXME: We should perhaps introduce a separate module for utilities like this +#include "../../communication/inc/coap_api.h" +#include "system_error.h" + #include +#include #if HAL_PLATFORM_FILESYSTEM #include "filesystem.h" @@ -48,6 +53,26 @@ static bool read_file_callback(pb_istream_t* strm, uint8_t* data, size_t size) { #endif // HAL_PLATFORM_FILESYSTEM +static bool read_coap_message_callback(pb_istream_t* strm, uint8_t* data, size_t size) { + size_t n = size; + int r = coap_read_payload((coap_message*)strm->state, data, &n, NULL /* block_cb */, NULL /* error_cb */, + NULL /* arg */, NULL /* reserved */); + if (r != 0 || n != size) { // COAP_RESULT_WAIT_BLOCK is treated as an error + return false; + } + return true; +} + +static bool write_coap_message_callback(pb_ostream_t* strm, const uint8_t* data, size_t size) { + size_t n = size; + int r = coap_write_payload((coap_message*)strm->state, data, &n, NULL /* block_cb */, NULL /* error_cb */, + NULL /* arg */, NULL /* reserved */); + if (r != 0 || n != size) { // COAP_RESULT_WAIT_BLOCK is treated as an error + return false; + } + return true; +} + pb_ostream_t* pb_ostream_init(void* reserved) { return (pb_ostream_t*)calloc(sizeof(pb_ostream_t), 1); } @@ -96,39 +121,65 @@ bool pb_istream_from_buffer_ex(pb_istream_t* stream, const pb_byte_t *buf, size_ #if HAL_PLATFORM_FILESYSTEM -bool pb_ostream_from_file(pb_ostream_t* stream, lfs_file_t* file, void* reserved) { +int pb_ostream_from_file(pb_ostream_t* stream, lfs_file_t* file, void* reserved) { if (!stream || !file) { - return false; + return SYSTEM_ERROR_INVALID_ARGUMENT; } filesystem_t* const fs = filesystem_get_instance(FILESYSTEM_INSTANCE_DEFAULT, NULL); if (!fs) { - return false; + return SYSTEM_ERROR_FILESYSTEM; } memset(stream, 0, sizeof(pb_ostream_t)); stream->callback = write_file_callback; stream->state = file; stream->max_size = SIZE_MAX; - return true; + return 0; } -bool pb_istream_from_file(pb_istream_t* stream, lfs_file_t* file, void* reserved) { +int pb_istream_from_file(pb_istream_t* stream, lfs_file_t* file, int size, void* reserved) { if (!stream || !file) { - return false; + return SYSTEM_ERROR_INVALID_ARGUMENT; } - filesystem_t* const fs = filesystem_get_instance(FILESYSTEM_INSTANCE_DEFAULT, NULL); - if (!fs) { - return false; - } - const lfs_soff_t pos = lfs_file_tell(&fs->instance, file); - const lfs_soff_t size = lfs_file_size(&fs->instance, file); - if (pos < 0 || size < 0) { - return false; + if (size < 0) { + filesystem_t* const fs = filesystem_get_instance(FILESYSTEM_INSTANCE_DEFAULT, NULL); + if (!fs) { + return SYSTEM_ERROR_FILESYSTEM; + } + lfs_soff_t pos = lfs_file_tell(&fs->instance, file); + if (pos < 0) { + return filesystem_to_system_error(pos); + } + lfs_soff_t sz = lfs_file_size(&fs->instance, file); + if (sz < 0) { + return filesystem_to_system_error(sz); + } + size = sz - pos; } memset(stream, 0, sizeof(pb_istream_t)); stream->callback = read_file_callback; stream->state = file; - stream->bytes_left = size - pos; - return true; + stream->bytes_left = size; + return 0; } #endif // HAL_PLATFORM_FILESYSTEM + +int pb_istream_from_coap_message(pb_istream_t* stream, coap_message* msg, void* reserved) { + int size = coap_peek_payload(msg, NULL /* data */, SIZE_MAX, NULL /* reserved */); + if (size < 0) { + return size; + } + memset(stream, 0, sizeof(*stream)); + stream->callback = read_coap_message_callback; + stream->state = msg; + stream->bytes_left = size; + return 0; +} + +int pb_ostream_from_coap_message(pb_ostream_t* stream, coap_message* msg, void* reserved) { + memset(stream, 0, sizeof(*stream)); + stream->callback = write_coap_message_callback; + stream->state = msg; + stream->max_size = COAP_BLOCK_SIZE; + return 0; +} diff --git a/system-dynalib/src/system_dynalib_ledger.c b/system-dynalib/src/system_dynalib_ledger.c new file mode 100644 index 0000000000..4ded8b7a96 --- /dev/null +++ b/system-dynalib/src/system_dynalib_ledger.c @@ -0,0 +1,3 @@ +#define DYNALIB_IMPORT + +#include "system_dynalib_ledger.h" diff --git a/system/inc/active_object.h b/system/inc/active_object.h index edcf618d10..af2af4e90f 100644 --- a/system/inc/active_object.h +++ b/system/inc/active_object.h @@ -499,13 +499,19 @@ class ISRTaskQueue { struct Task { TaskFunc func; Task* next; // Next element in the queue + Task* prev; // Previous element in the queue explicit Task(TaskFunc func = nullptr) : func(func), - next(nullptr) { + next(nullptr), + prev(nullptr) { } virtual ~Task() = default; + + // Task objects are not copyable nor movable + Task(const Task&) = delete; + Task& operator=(const Task&) = delete; }; ISRTaskQueue() : @@ -513,7 +519,25 @@ class ISRTaskQueue { lastTask_(nullptr) { } + /** + * Add a task object to the queue. + * + * Calling this method is a no-op if the task object is already in the queue. + */ void enqueue(Task* task); + + /** + * Remove a task object from the queue. + * + * Calling this method is a no-op if the task object is not in the queue. + */ + void remove(Task* task); + + /** + * Process the next task in the queue. + * + * @return `true` if the task was processed, or `false` if the queue was empty. + */ bool process(); private: diff --git a/system/inc/system_dynalib_ledger.h b/system/inc/system_dynalib_ledger.h new file mode 100644 index 0000000000..ed001d99f1 --- /dev/null +++ b/system/inc/system_dynalib_ledger.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include "hal_platform.h" + +#if HAL_PLATFORM_LEDGER + +#include "dynalib.h" + +#ifdef DYNALIB_EXPORT +#include "system_ledger.h" +#endif + +DYNALIB_BEGIN(system_ledger) + +DYNALIB_FN(0, system_ledger, ledger_get_instance, int(ledger_instance**, const char*, void*)) +DYNALIB_FN(1, system_ledger, ledger_add_ref, void(ledger_instance*, void*)) +DYNALIB_FN(2, system_ledger, ledger_release, void(ledger_instance*, void*)) +DYNALIB_FN(3, system_ledger, ledger_lock, void(ledger_instance*, void*)) +DYNALIB_FN(4, system_ledger, ledger_unlock, void(ledger_instance*, void*)) +DYNALIB_FN(5, system_ledger, ledger_set_callbacks, void(ledger_instance*, const ledger_callbacks*, void*)) +DYNALIB_FN(6, system_ledger, ledger_set_app_data, void(ledger_instance*, void*, ledger_destroy_app_data_callback, void*)) +DYNALIB_FN(7, system_ledger, ledger_get_app_data, void*(ledger_instance*, void*)) +DYNALIB_FN(8, system_ledger, ledger_get_info, int(ledger_instance*, ledger_info*, void*)) +DYNALIB_FN(9, system_ledger, ledger_open, int(ledger_stream**, ledger_instance*, int, void*)) +DYNALIB_FN(10, system_ledger, ledger_close, int(ledger_stream*, int, void*)) +DYNALIB_FN(11, system_ledger, ledger_read, int(ledger_stream*, char*, size_t, void*)) +DYNALIB_FN(12, system_ledger, ledger_write, int(ledger_stream*, const char*, size_t, void*)) +DYNALIB_FN(13, system_ledger, ledger_purge, int(const char*, void*)) +DYNALIB_FN(14, system_ledger, ledger_purge_all, int(void*)) +DYNALIB_FN(15, system_ledger, ledger_get_names, int(char***, size_t*, void*)) + +DYNALIB_END(system_ledger) + +#endif // HAL_PLATFORM_LEDGER diff --git a/system/inc/system_ledger.h b/system/inc/system_ledger.h new file mode 100644 index 0000000000..b8fde781bc --- /dev/null +++ b/system/inc/system_ledger.h @@ -0,0 +1,325 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include "hal_platform.h" + +#if HAL_PLATFORM_LEDGER + +#include +#include + +/** + * Ledger API version. + */ +#define LEDGER_API_VERSION 1 + +/** + * Maximum length of a ledger name. + */ +#define LEDGER_MAX_NAME_LENGTH 32 + +/** + * Maximum size of ledger data. + */ +#define LEDGER_MAX_DATA_SIZE 16384 + +/** + * Ledger instance. + */ +typedef struct ledger_instance ledger_instance; + +/** + * Stream instance. + */ +typedef struct ledger_stream ledger_stream; + +/** + * Callback invoked when a ledger has been synchronized with the Cloud. + * + * @param ledger Ledger instance. + * @param app_data Application data. + */ +typedef void (*ledger_sync_callback)(ledger_instance* ledger, void* app_data); + +/** + * Callback invoked to destroy the application data associated with a ledger instance. + * + * @param app_data Application data. + */ +typedef void (*ledger_destroy_app_data_callback)(void* app_data); + +/** + * Ledger scope. + */ +typedef enum ledger_scope { + LEDGER_SCOPE_UNKNOWN = 0, ///< Unknown scope. + LEDGER_SCOPE_DEVICE = 1, ///< Device scope. + LEDGER_SCOPE_PRODUCT = 2, ///< Product scope. + LEDGER_SCOPE_OWNER = 3 ///< Owner scope. +} ledger_scope; + +/** + * Sync direction. + */ +typedef enum ledger_sync_direction { + LEDGER_SYNC_DIRECTION_UNKNOWN = 0, ///< Unknown direction. + LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD = 1, ///< Device to cloud. + LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE = 2 ///< Cloud to device. +} ledger_sync_direction; + +/** + * Ledger info flags. + */ +typedef enum ledger_info_flag { + LEDGER_INFO_SYNC_PENDING = 0x01 ///< Ledger has changes that have not yet been synchronized with the Cloud. +} ledger_info_flag; + +/** + * Stream mode flags. + */ +typedef enum ledger_stream_mode { + LEDGER_STREAM_MODE_READ = 0x01, ///< Open for reading. + LEDGER_STREAM_MODE_WRITE = 0x02 ///< Open for writing. +} ledger_stream_mode; + +/** + * Stream close flags. + */ +typedef enum ledger_stream_close_flag { + LEDGER_STREAM_CLOSE_DISCARD = 0x01 ///< Discard any written data. +} ledger_stream_close_flag; + +/** + * Ledger callbacks. + */ +typedef struct ledger_callbacks { + int version; ///< API version. Must be set to `LEDGER_API_VERSION`. + /** + * Callback invoked when the ledger has been synchronized with the Cloud. + */ + ledger_sync_callback sync; +} ledger_callbacks; + +/** + * Ledger info. + */ +typedef struct ledger_info { + int version; ///< API version. Must be set to `LEDGER_API_VERSION`. + const char* name; ///< Ledger name. + /** + * Time the ledger was last updated, in milliseconds since the Unix epoch. + * + * If 0, the time is unknown. + */ + int64_t last_updated; + /** + * Time the ledger was last synchronized with the Cloud, in milliseconds since the Unix epoch. + * + * If 0, the ledger has never been synchronized. + */ + int64_t last_synced; + size_t data_size; ///< Size of the ledger data in bytes. + int scope; ///< Ledger scope as defined by the `ledger_scope` enum. + int sync_direction; ///< Synchronization direction as defined by the `ledger_sync_direction` enum. + int flags; ///< Flags defined by the `ledger_info_flag` enum. +} ledger_info; + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Get a ledger instance. + * + * @param ledger[out] Ledger instance. + * @param name Ledger name. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int ledger_get_instance(ledger_instance** ledger, const char* name, void* reserved); + +/** + * Increment a ledger's reference count. + * + * @param ledger Ledger instance. + * @param reserved Reserved argument. Must be set to `NULL`. + */ +void ledger_add_ref(ledger_instance* ledger, void* reserved); + +/** + * Decrement a ledger's reference count. + * + * The ledger instance is destroyed when its reference count reaches 0. + * + * @param ledger Ledger instance. + * @param reserved Reserved argument. Must be set to `NULL`. + */ +void ledger_release(ledger_instance* ledger, void* reserved); + +/** + * Lock a ledger instance. + * + * The instance can be locked recursively by the same thread. + * + * @param ledger Ledger instance. + * @param reserved Reserved argument. Must be set to `NULL`. + */ +void ledger_lock(ledger_instance* ledger, void* reserved); + +/** + * Unlock a ledger instance. + * + * @param ledger Ledger instance. + * @param reserved Reserved argument. Must be set to `NULL`. + */ +void ledger_unlock(ledger_instance* ledger, void* reserved); + +/** + * Set the ledger callbacks. + * + * All callbacks are invoked in the system thread. + * + * @param ledger Ledger instance. + * @param callbacks Ledger callbacks. Can be set to `NULL` to clear all currently registered callbacks. + * @param reserved Reserved argument. Must be set to `NULL`. + */ +void ledger_set_callbacks(ledger_instance* ledger, const ledger_callbacks* callbacks, void* reserved); + +/** + * Attach application-specific data to a ledger instance. + * + * If a destructor callback is provided, it will be invoked when the ledger instance is destroyed. + * The calling code is responsible for destroying the old application data if it was already set for + * this ledger instance. + * + * @param ledger Ledger instance. + * @param app_data Application data. + * @param destroy Destructor callback. Can be `NULL`. + * @param reserved Reserved argument. Must be set to `NULL`. + */ +void ledger_set_app_data(ledger_instance* ledger, void* app_data, ledger_destroy_app_data_callback destroy, + void* reserved); + +/** + * Get application-specific data associated with a ledger instance. + * + * @param ledger Ledger instance. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return Application data. + */ +void* ledger_get_app_data(ledger_instance* ledger, void* reserved); + +/** + * Get ledger info. + * + * @param ledger Ledger instance. + * @param[out] info Ledger info to be populated. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int ledger_get_info(ledger_instance* ledger, ledger_info* info, void* reserved); + +/** + * Open a ledger for reading or writing. + * + * @param[out] stream Stream instance. + * @param ledger Ledger instance. + * @param mode Flags defined by the `ledger_stream_mode` enum. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int ledger_open(ledger_stream** stream, ledger_instance* ledger, int mode, void* reserved); + +/** + * Close a stream. + * + * The stream instance is destroyed even if an error occurs while closing the stream. + * + * @param stream Stream instance. + * @param flags Flags defined by the `ledger_stream_close_flag` enum. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int ledger_close(ledger_stream* stream, int flags, void* reserved); + +/** + * Read from a stream. + * + * @param stream Stream instance. + * @param[out] data Output buffer. + * @param size Maximum number of bytes to read. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return Number of bytes read or an error code defined by the `system_error_t` enum. + */ +int ledger_read(ledger_stream* stream, char* data, size_t size, void* reserved); + +/** + * Write to a stream. + * + * @param stream Stream instance. + * @param data Input buffer. + * @param size Number of bytes to write. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return Number of bytes written or an error code defined by the `system_error_t` enum. + */ +int ledger_write(ledger_stream* stream, const char* data, size_t size, void* reserved); + +/** + * Get the names of all local ledgers. + * + * @param[out] names Array of ledger names. The calling code is responsible for freeing the allocated + * array as well as its individual elements via `free()`. + * @param[out] count Number of elements in the array. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int ledger_get_names(char*** names, size_t* count, void* reserved); + +/** + * Remove any local data associated with a ledger. + * + * The device must not be connected to the Cloud. The operation will fail if the ledger with the + * given name is in use. + * + * @note The data is not guaranteed to be removed in an irrecoverable way. + * + * @param name Ledger name. + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int ledger_purge(const char* name, void* reserved); + +/** + * Remove any local data associated with existing ledgers. + * + * The device must not be connected to the Cloud. The operation will fail if any of the existing + * ledgers is in use. + * + * @note The data is not guaranteed to be removed in an irrecoverable way. + * + * @param reserved Reserved argument. Must be set to `NULL`. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ +int ledger_purge_all(void* reserved); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // HAL_PLATFORM_LEDGER diff --git a/system/inc/system_task.h b/system/inc/system_task.h index 38d3947025..184ed2e230 100644 --- a/system/inc/system_task.h +++ b/system/inc/system_task.h @@ -32,8 +32,12 @@ #include "wlan_hal.h" #ifdef __cplusplus + +#include + extern "C" { -#endif + +#endif // defined(__cplusplus) uint32_t HAL_NET_SetNetWatchDog(uint32_t timeOutInuS); void Network_Setup(bool threaded); @@ -117,7 +121,51 @@ typedef int (*system_task_fn)(); int system_isr_task_queue_free_memory(void *ptrToFree); #ifdef __cplusplus + +} // extern "C" + +namespace particle::system { + +/** + * Allocate an object in the system pool. + * + * This function can be called from an ISR. + * + * @tparam T Object type. + * @param ... Arguments to pass to the constructor of `T`. + * @return Pointer to the object. + * + * @see `systemPoolDelete` + */ +template +inline T* systemPoolNew(ArgsT&&... args) { + auto p = static_cast(system_pool_alloc(sizeof(T), nullptr /* reserved */)); + if (p) { + new(p) T(std::forward(args)...); + } + return p; } -#endif + +/** + * Destroy an object allocated in the system pool. + * + * This function can be called from an ISR. + * + * @tparam T Object type. + * @param p Pointer to the object. + * + * @see `systemPoolNew` + */ +template +inline void systemPoolDelete(T* p) { + if (p) { + p->~T(); + system_pool_free(p, nullptr /* reserved */); + } +} + +} // namespace particle::system + +#endif // defined(__cplusplus) #endif /*__SPARK_WLAN_H*/ diff --git a/system/src/active_object.cpp b/system/src/active_object.cpp index 3d4a16a9b5..6cc6361314 100644 --- a/system/src/active_object.cpp +++ b/system/src/active_object.cpp @@ -98,6 +98,9 @@ void ActiveObjectBase::run_active_object(void* data) void ISRTaskQueue::enqueue(Task* task) { ATOMIC_BLOCK() { + if (task->next || task->prev || lastTask_ == task) { + return; // Task object is already in the queue + } // Add task object to the queue if (lastTask_) { lastTask_->next = task; @@ -105,6 +108,7 @@ void ISRTaskQueue::enqueue(Task* task) { firstTask_ = task; } task->next = nullptr; + task->prev = lastTask_; lastTask_ = task; } // FIXME: some other feature flag? @@ -113,16 +117,41 @@ void ISRTaskQueue::enqueue(Task* task) { #endif // HAL_PLATFORM_SOCKET_IOCTL_NOTIFY } +void ISRTaskQueue::remove(Task* task) { + ATOMIC_BLOCK() { + auto next = task->next; + auto prev = task->prev; + if (next) { + next->prev = prev; + task->next = nullptr; + } else if (lastTask_ == task) { + lastTask_ = prev; + } + if (prev) { + prev->next = next; + task->prev = nullptr; + } else if (firstTask_ == task) { + firstTask_ = next; + } + } +} + bool ISRTaskQueue::process() { - Task* task = nullptr; if (!firstTask_) { return false; } + Task* task = nullptr; ATOMIC_BLOCK() { + if (!firstTask_) { + return false; + } // Take task object from the queue task = firstTask_; firstTask_ = task->next; - if (!firstTask_) { + if (firstTask_) { + firstTask_->prev = nullptr; + task->next = nullptr; + } else { lastTask_ = nullptr; } } diff --git a/system/src/ledger/ledger.cpp b/system/src/ledger/ledger.cpp new file mode 100644 index 0000000000..b1eca0a047 --- /dev/null +++ b/system/src/ledger/ledger.cpp @@ -0,0 +1,891 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#if !defined(DEBUG_BUILD) && !defined(UNIT_TEST) +#define NDEBUG // TODO: Define NDEBUG in release builds +#endif + +#include "hal_platform.h" + +#if HAL_PLATFORM_LEDGER + +#include +#include + +#include "ledger.h" +#include "ledger_manager.h" +#include "ledger_util.h" + +#include "file_util.h" +#include "time_util.h" +#include "endian_util.h" +#include "scope_guard.h" +#include "check.h" + +#include "cloud/ledger.pb.h" // Cloud protocol definitions +#include "ledger.pb.h" // Internal definitions + +#define PB_LEDGER(_name) particle_cloud_ledger_##_name +#define PB_INTERNAL(_name) particle_firmware_##_name + +LOG_SOURCE_CATEGORY("system.ledger"); + +namespace particle { + +using fs::FsLock; + +namespace system { + +static_assert(LEDGER_SCOPE_UNKNOWN == (int)PB_LEDGER(ScopeType_SCOPE_TYPE_UNKNOWN) && + LEDGER_SCOPE_DEVICE == (int)PB_LEDGER(ScopeType_SCOPE_TYPE_DEVICE) && + LEDGER_SCOPE_PRODUCT == (int)PB_LEDGER(ScopeType_SCOPE_TYPE_PRODUCT) && + LEDGER_SCOPE_OWNER == (int)PB_LEDGER(ScopeType_SCOPE_TYPE_OWNER)); + +static_assert(LEDGER_SYNC_DIRECTION_UNKNOWN == (int)PB_LEDGER(SyncDirection_SYNC_DIRECTION_UNKNOWN) && + LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD == (int)PB_LEDGER(SyncDirection_SYNC_DIRECTION_DEVICE_TO_CLOUD) && + LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE == (int)PB_LEDGER(SyncDirection_SYNC_DIRECTION_CLOUD_TO_DEVICE)); + +static_assert(LEDGER_MAX_NAME_LENGTH + 1 == sizeof(PB_INTERNAL(LedgerInfo::name)) && + LEDGER_MAX_NAME_LENGTH + 1 == sizeof(PB_LEDGER(GetInfoResponse_Ledger::name))); + +static_assert(MAX_LEDGER_SCOPE_ID_SIZE == sizeof(PB_INTERNAL(LedgerInfo_scope_id_t::bytes)) && + MAX_LEDGER_SCOPE_ID_SIZE == sizeof(PB_LEDGER(GetInfoResponse_Ledger_scope_id_t::bytes))); + +namespace { + +/* + The ledger directory is organized as follows: + + /usr/ledger/ + | + +--- my-ledger/ - Files of the ledger called "my-ledger" + | | + | +--- staged/ - Fully written ledger data files + | | +--- 0001 + | | +--- 0002 + | | +--- ... + | | + | +--- temp/ - Partially written ledger data files + | | +--- 0003 + | | +--- 0004 + | | +--- ... + | | + | +--- current - Current ledger data + | + +--- ... + + When a ledger is opened for writing, a temporary file for the new ledger data is created in the + "temp" directory. When writing is finished, if there are active readers of the ledger data, the + temporary file is moved to the "staged" directory, otherwise the file is moved to "current" to + replace the current ledger data. + + When a ledger is opened for reading, it is checked whether the most recent ledger data is stored + in "current" or a file in the "staged" directory. The ledger data is then read from the respective + file. + + The ledger instance tracks the readers of the current and staged data to ensure that the relevant + files are modified or removed only when the last reader accessing them is closed. When all readers + are closed, the most recent staged data is moved to "current" and all other files in "staged" are + removed. +*/ +const auto TEMP_DATA_DIR_NAME = "temp"; +const auto STAGED_DATA_DIR_NAME = "staged"; +const auto CURRENT_DATA_FILE_NAME = "current"; + +const unsigned DATA_FORMAT_VERSION = 1; + +const size_t MAX_PATH_LEN = 127; + +// Internal result codes +enum Result { + CURRENT_DATA_NOT_FOUND = 1 +}; + +/* + The layout of a ledger data file: + + Field | Size | Description + ----------+-----------+------------ + data | data_size | Contents of the ledger in an application-specific format + info | info_size | Protobuf-encoded ledger info (particle.firmware.LedgerInfo) + data_size | 4 | Size of the "data" field (unsigned integer) + info_size | 4 | Size of the "info" field (unsigned integer) + version | 4 | Format version number (unsigned integer) + + All integer fields are encoded in little-endian byte order. +*/ +struct LedgerDataFooter { + uint32_t dataSize; + uint32_t infoSize; + uint32_t version; +} __attribute__((packed)); + +int readFooter(lfs_t* fs, lfs_file_t* file, size_t* dataSize = nullptr, size_t* infoSize = nullptr, int* version = nullptr) { + LedgerDataFooter f = {}; + size_t n = CHECK_FS(lfs_file_read(fs, file, &f, sizeof(f))); + if (n != sizeof(f)) { + LOG(ERROR, "Unexpected end of ledger data file"); + return SYSTEM_ERROR_LEDGER_INVALID_FORMAT; + } + if (dataSize) { + *dataSize = littleEndianToNative(f.dataSize); + } + if (infoSize) { + *infoSize = littleEndianToNative(f.infoSize); + } + if (version) { + *version = littleEndianToNative(f.version); + } + return n; +} + +int writeFooter(lfs_t* fs, lfs_file_t* file, size_t dataSize, size_t infoSize, int version = DATA_FORMAT_VERSION) { + LedgerDataFooter f = {}; + f.dataSize = nativeToLittleEndian(dataSize); + f.infoSize = nativeToLittleEndian(infoSize); + f.version = nativeToLittleEndian(version); + size_t n = CHECK_FS(lfs_file_write(fs, file, &f, sizeof(f))); + if (n != sizeof(f)) { + LOG(ERROR, "Unexpected number of bytes written"); + return SYSTEM_ERROR_FILESYSTEM; + } + return n; +} + +int writeLedgerInfo(lfs_t* fs, lfs_file_t* file, const char* ledgerName, const LedgerInfo& info) { + // All fields must be set + assert(info.isScopeTypeSet() && info.isScopeIdSet() && info.isSyncDirectionSet() && info.isDataSizeSet() && + info.isLastUpdatedSet() && info.isLastSyncedSet() && info.isUpdateCountSet() && info.isSyncPendingSet()); + PB_INTERNAL(LedgerInfo) pbInfo = {}; + size_t n = strlcpy(pbInfo.name, ledgerName, sizeof(pbInfo.name)); + if (n >= sizeof(pbInfo.name)) { + return SYSTEM_ERROR_INTERNAL; // Name is longer than the maximum size specified in ledger.proto + } + auto& scopeId = info.scopeId(); + assert(scopeId.size <= sizeof(pbInfo.scope_id.bytes)); + std::memcpy(pbInfo.scope_id.bytes, scopeId.data, scopeId.size); + pbInfo.scope_id.size = scopeId.size; + pbInfo.scope_type = static_cast(info.scopeType()); + pbInfo.sync_direction = static_cast(info.syncDirection()); + if (info.lastUpdated()) { + pbInfo.last_updated = info.lastUpdated(); + pbInfo.has_last_updated = true; + } + if (info.lastSynced()) { + pbInfo.last_synced = info.lastSynced(); + pbInfo.has_last_synced = true; + } + pbInfo.update_count = info.updateCount(); + pbInfo.sync_pending = info.syncPending(); + n = CHECK(encodeProtobufToFile(file, &PB_INTERNAL(LedgerInfo_msg), &pbInfo)); + return n; +} + +inline int getTempDirPath(char* buf, size_t size, const char* ledgerName) { + CHECK(formatLedgerPath(buf, size, ledgerName, "%s/", TEMP_DATA_DIR_NAME)); + return 0; +} + +inline int getTempFilePath(char* buf, size_t size, const char* ledgerName, int seqNum) { + CHECK(formatLedgerPath(buf, size, ledgerName, "%s/%04d", TEMP_DATA_DIR_NAME, seqNum)); + return 0; +} + +inline int getTempFilePath(char* buf, size_t size, const char* ledgerName, const char* fileName) { + CHECK(formatLedgerPath(buf, size, ledgerName, "%s/%s", TEMP_DATA_DIR_NAME, fileName)); + return 0; +} + +inline int getStagedDirPath(char* buf, size_t size, const char* ledgerName) { + CHECK(formatLedgerPath(buf, size, ledgerName, "%s/", STAGED_DATA_DIR_NAME)); + return 0; +} + +inline int getStagedFilePath(char* buf, size_t size, const char* ledgerName, int seqNum) { + CHECK(formatLedgerPath(buf, size, ledgerName, "%s/%04d", STAGED_DATA_DIR_NAME, seqNum)); + return 0; +} + +inline int getStagedFilePath(char* buf, size_t size, const char* ledgerName, const char* fileName) { + CHECK(formatLedgerPath(buf, size, ledgerName, "%s/%s", STAGED_DATA_DIR_NAME, fileName)); + return 0; +} + +inline int getCurrentFilePath(char* buf, size_t size, const char* ledgerName) { + CHECK(formatLedgerPath(buf, size, ledgerName, "%s", CURRENT_DATA_FILE_NAME)); + return 0; +} + +bool isLedgerNameValid(const char* name) { + size_t len = 0; + char c = 0; + while ((c = *name++)) { + if (!(++len <= LEDGER_MAX_NAME_LENGTH && ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-'))) { + return false; + } + } + if (len == 0) { + return false; + } + return true; +} + +// Helper functions that transform LittleFS errors to a system error +inline int renameFile(lfs_t* fs, const char* oldPath, const char* newPath) { + CHECK_FS(lfs_rename(fs, oldPath, newPath)); + return 0; +} + +inline int removeFile(lfs_t* fs, const char* path) { + CHECK_FS(lfs_remove(fs, path)); + return 0; +} + +inline int closeFile(lfs_t* fs, lfs_file_t* file) { + CHECK_FS(lfs_file_close(fs, file)); + return 0; +} + +inline int closeDir(lfs_t* fs, lfs_dir_t* dir) { + CHECK_FS(lfs_dir_close(fs, dir)); + return 0; +} + +} // namespace + +Ledger::Ledger(detail::LedgerSyncContext* ctx) : + LedgerBase(ctx), + lastSeqNum_(0), + stagedSeqNum_(0), + curReaderCount_(0), + stagedReaderCount_(0), + stagedFileCount_(0), + lastUpdated_(0), + lastSynced_(0), + dataSize_(0), + updateCount_(0), + syncPending_(false), + syncCallback_(nullptr), + destroyAppData_(nullptr), + appData_(nullptr), + name_(""), + scopeId_(EMPTY_LEDGER_SCOPE_ID), + scopeType_(LEDGER_SCOPE_UNKNOWN), + syncDir_(LEDGER_SYNC_DIRECTION_UNKNOWN), + inited_(false) { +} + +Ledger::~Ledger() { + if (appData_ && destroyAppData_) { + destroyAppData_(appData_); + } +} + +int Ledger::init(const char* name) { + if (!isLedgerNameValid(name)) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + name_ = name; + FsLock fs; + int r = CHECK(loadLedgerInfo(fs.instance())); + if (r == Result::CURRENT_DATA_NOT_FOUND) { + // Initialize the ledger directory + char path[MAX_PATH_LEN + 1]; + CHECK(getTempDirPath(path, sizeof(path), name_)); + CHECK(mkdirp(path)); + CHECK(getStagedDirPath(path, sizeof(path), name_)); + CHECK(mkdirp(path)); + // Create a file for the current ledger data + CHECK(initCurrentData(fs.instance())); + } else { + // Clean up and recover staged data + CHECK(removeTempData(fs.instance())); + r = CHECK(flushStagedData(fs.instance())); + if (r > 0) { + // Found staged data. Reload the ledger info + CHECK(loadLedgerInfo(fs.instance())); + } + } + inited_ = true; + return 0; +} + +int Ledger::initReader(LedgerReader& reader) { + // Reference counter of this ledger instance is managed by the LedgerManager so if it needs + // to be incremented it's important to do so before acquiring a lock on the ledger instance + // to avoid a deadlock + RefCountPtr ledgerPtr(this); + std::lock_guard lock(*this); + if (!inited_) { + return SYSTEM_ERROR_INVALID_STATE; + } + CHECK(reader.init(info(), stagedSeqNum_, std::move(ledgerPtr))); + if (stagedSeqNum_ > 0) { + ++stagedReaderCount_; + } else { + ++curReaderCount_; + } + return 0; +} + +int Ledger::initWriter(LedgerWriter& writer, LedgerWriteSource src) { + RefCountPtr ledgerPtr(this); // See initReader() + std::lock_guard lock(*this); + if (!inited_) { + return SYSTEM_ERROR_INVALID_STATE; + } + // It's allowed to write to a ledger while its sync direction is unknown + if (src == LedgerWriteSource::USER && syncDir_ != LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD && + syncDir_ != LEDGER_SYNC_DIRECTION_UNKNOWN) { + return SYSTEM_ERROR_LEDGER_READ_ONLY; + } + CHECK(writer.init(src, ++lastSeqNum_, std::move(ledgerPtr))); + return 0; +} + +LedgerInfo Ledger::info() const { + std::lock_guard lock(*this); + return LedgerInfo() + .scopeType(scopeType_) + .scopeId(scopeId_) + .syncDirection(syncDir_) + .dataSize(dataSize_) + .lastUpdated(lastUpdated_) + .lastSynced(lastSynced_) + .updateCount(updateCount_) + .syncPending(syncPending_); +} + +int Ledger::updateInfo(const LedgerInfo& info) { + std::lock_guard lock(*this); + // Open the file with the current ledger data + char path[MAX_PATH_LEN + 1]; + CHECK(getCurrentFilePath(path, sizeof(path), name_)); + FsLock fs; + lfs_file_t file = {}; + // TODO: Rewriting parts of a file is inefficient with LittleFS. Consider using an append-only + // structure for storing ledger data + CHECK_FS(lfs_file_open(fs.instance(), &file, path, LFS_O_RDWR)); + NAMED_SCOPE_GUARD(closeFileGuard, { + int r = closeFile(fs.instance(), &file); + if (r < 0) { + LOG(ERROR, "Error while closing file: %d", r); + } + }); + // Read the footer + size_t dataSize = 0; + size_t infoSize = 0; + CHECK_FS(lfs_file_seek(fs.instance(), &file, -(int)sizeof(LedgerDataFooter), LFS_SEEK_END)); + CHECK(readFooter(fs.instance(), &file, &dataSize, &infoSize)); + // Write the info section + CHECK_FS(lfs_file_seek(fs.instance(), &file, -(int)(infoSize + sizeof(LedgerDataFooter)), LFS_SEEK_END)); + auto newInfo = this->info().update(info); + size_t newInfoSize = CHECK(writeLedgerInfo(fs.instance(), &file, name_, newInfo)); + // Write the footer + if (newInfoSize != infoSize) { + CHECK(writeFooter(fs.instance(), &file, dataSize, newInfoSize)); + size_t newFileSize = CHECK_FS(lfs_file_tell(fs.instance(), &file)); + CHECK_FS(lfs_file_truncate(fs.instance(), &file, newFileSize)); + } + closeFileGuard.dismiss(); + CHECK_FS(lfs_file_close(fs.instance(), &file)); + // Update the instance + setLedgerInfo(newInfo); + return 0; +} + +void Ledger::notifySynced() { + std::lock_guard lock(*this); + if (syncCallback_) { + syncCallback_(reinterpret_cast(this), appData_); + } +} + +int Ledger::notifyReaderClosed(bool staged) { + std::lock_guard lock(*this); + if (staged) { + --stagedReaderCount_; + } else { + --curReaderCount_; + } + // Check if there's staged data and if it can be flushed + int result = 0; + if (stagedSeqNum_ > 0 && curReaderCount_ == 0 && stagedReaderCount_ == 0) { + // To save on RAM usage, we don't count how many readers are open for each individual file, + // which may result in the staged directory containing multiple unused files with outdated + // ledger data. Typically, however, when the last reader is closed, there will be at most + // one file with staged data as the application never keeps ledger streams open for long. + // The only slow reader is the system which streams ledger data to the server + FsLock fs; + if (stagedFileCount_ == 1) { + // Fast track: there's one file with staged data. Move it to "current" + char srcPath[MAX_PATH_LEN + 1]; + char destPath[MAX_PATH_LEN + 1]; + CHECK(getStagedFilePath(srcPath, sizeof(srcPath), name_, stagedSeqNum_)); + CHECK(getCurrentFilePath(destPath, sizeof(destPath), name_)); + result = renameFile(fs.instance(), srcPath, destPath); + if (result < 0) { + // The filesystem is full or broken, or somebody messed up the staged directory + LOG(ERROR, "Failed to rename file: %d", result); + int r = removeFile(fs.instance(), srcPath); + if (r < 0) { + LOG(WARN, "Failed to remove file: %d", r); + } + } + } else { + // The staged directory contains outdated files. Clean those up and move the most recent + // file to "current" + int r = flushStagedData(fs.instance()); + if (r < 0) { + result = r; + } else if (r != stagedSeqNum_) { + // The file moved wasn't the most recent one for some reason + LOG(ERROR, "Staged ledger data not found"); + result = SYSTEM_ERROR_LEDGER_INCONSISTENT_STATE; + } + } + if (result < 0) { + // Try recovering some known consistent state + int r = loadLedgerInfo(fs.instance()); + if (r < 0) { + LOG(ERROR, "Failed to recover ledger state"); + } + } + stagedSeqNum_ = 0; + stagedFileCount_ = 0; + } + return result; +} + +int Ledger::notifyWriterClosed(const LedgerInfo& info, int tempSeqNum) { + std::unique_lock lock(*this); + FsLock fs; + // Move the file where appropriate + bool newStagedFile = false; + char srcPath[MAX_PATH_LEN + 1]; + char destPath[MAX_PATH_LEN + 1]; + CHECK(getTempFilePath(srcPath, sizeof(srcPath), name_, tempSeqNum)); + if (curReaderCount_ == 0 && stagedReaderCount_ == 0) { + // Nobody is reading anything. Move to "current" + CHECK(getCurrentFilePath(destPath, sizeof(destPath), name_)); + } else if (stagedSeqNum_ > 0 && stagedReaderCount_ == 0) { + // There's staged data and nobody is reading it. Replace it + CHECK(getStagedFilePath(destPath, sizeof(destPath), name_, stagedSeqNum_)); + } else { + // Create a new file in "staged". Note that in this case, "current" can't be replaced with + // the new data even if nobody is reading it, as otherwise it would get overwritten with + // outdated data if the device resets before the staged directory is cleaned up + CHECK(getStagedFilePath(destPath, sizeof(destPath), name_, tempSeqNum)); + newStagedFile = true; + } + CHECK_FS(lfs_rename(fs.instance(), srcPath, destPath)); + if (newStagedFile) { + stagedSeqNum_ = tempSeqNum; + ++stagedFileCount_; + } + setLedgerInfo(this->info().update(info)); + return 0; +} + +int Ledger::loadLedgerInfo(lfs_t* fs) { + // Open the file with the current ledger data + char path[MAX_PATH_LEN + 1]; + CHECK(getCurrentFilePath(path, sizeof(path), name_)); + lfs_file_t file = {}; + int r = lfs_file_open(fs, &file, path, LFS_O_RDONLY); + if (r < 0) { + if (r == LFS_ERR_NOENT) { + return Result::CURRENT_DATA_NOT_FOUND; + } + CHECK_FS(r); // Forward the error + } + SCOPE_GUARD({ + int r = closeFile(fs, &file); + if (r < 0) { + LOG(ERROR, "Error while closing file: %d", r); + } + }); + // Read the footer + size_t dataSize = 0; + size_t infoSize = 0; + int version = 0; + CHECK_FS(lfs_file_seek(fs, &file, -(int)sizeof(LedgerDataFooter), LFS_SEEK_END)); + CHECK(readFooter(fs, &file, &dataSize, &infoSize, &version)); + if (version != DATA_FORMAT_VERSION) { + LOG(ERROR, "Unsupported version of ledger data format: %d", version); + return SYSTEM_ERROR_LEDGER_UNSUPPORTED_FORMAT; + } + size_t fileSize = CHECK_FS(lfs_file_size(fs, &file)); + if (fileSize != dataSize + infoSize + sizeof(LedgerDataFooter)) { + LOG(ERROR, "Unexpected size of ledger data file"); + return SYSTEM_ERROR_LEDGER_INVALID_FORMAT; + } + // Skip the data section + CHECK_FS(lfs_file_seek(fs, &file, dataSize, LFS_SEEK_SET)); + // Read the info section + PB_INTERNAL(LedgerInfo) pbInfo = {}; + r = decodeProtobufFromFile(&file, &PB_INTERNAL(LedgerInfo_msg), &pbInfo, infoSize); + if (r < 0) { + LOG(ERROR, "Failed to parse ledger info: %d", r); + return SYSTEM_ERROR_LEDGER_INVALID_FORMAT; + } + if (std::strcmp(pbInfo.name, name_) != 0) { + LOG(ERROR, "Unexpected ledger name"); + return SYSTEM_ERROR_LEDGER_INVALID_FORMAT; + } + assert(pbInfo.scope_id.size <= sizeof(scopeId_.data)); + std::memcpy(scopeId_.data, pbInfo.scope_id.bytes, pbInfo.scope_id.size); + scopeId_.size = pbInfo.scope_id.size; + scopeType_ = static_cast(pbInfo.scope_type); + syncDir_ = static_cast(pbInfo.sync_direction); + dataSize_ = dataSize; + lastUpdated_ = pbInfo.has_last_updated ? pbInfo.last_updated : 0; + lastSynced_ = pbInfo.has_last_synced ? pbInfo.last_synced : 0; + updateCount_ = pbInfo.update_count; + syncPending_ = pbInfo.sync_pending; + return 0; +} + +void Ledger::setLedgerInfo(const LedgerInfo& info) { + // All fields must be set + assert(info.isScopeTypeSet() && info.isScopeIdSet() && info.isSyncDirectionSet() && info.isDataSizeSet() && + info.isLastUpdatedSet() && info.isLastSyncedSet() && info.isUpdateCountSet() && info.isSyncPendingSet()); + scopeType_ = info.scopeType(); + scopeId_ = info.scopeId(); + syncDir_ = info.syncDirection(); + dataSize_ = info.dataSize(); + lastUpdated_ = info.lastUpdated(); + lastSynced_ = info.lastSynced(); + updateCount_ = info.updateCount(); + syncPending_ = info.syncPending(); +} + +int Ledger::initCurrentData(lfs_t* fs) { + // Create a file for the current ledger data + char path[MAX_PATH_LEN + 1]; + CHECK(getCurrentFilePath(path, sizeof(path), name_)); + lfs_file_t file = {}; + CHECK_FS(lfs_file_open(fs, &file, path, LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL)); + SCOPE_GUARD({ + int r = closeFile(fs, &file); + if (r < 0) { + LOG(ERROR, "Error while closing file: %d", r); + } + }); + // Write the info section + size_t infoSize = CHECK(writeLedgerInfo(fs, &file, name_, info())); + // Write the footer + CHECK(writeFooter(fs, &file, 0 /* dataSize */, infoSize)); + return 0; +} + +int Ledger::flushStagedData(lfs_t* fs) { + char path[MAX_PATH_LEN + 1]; + CHECK(getStagedDirPath(path, sizeof(path), name_)); + lfs_dir_t dir = {}; + CHECK_FS(lfs_dir_open(fs, &dir, path)); + SCOPE_GUARD({ + int r = closeDir(fs, &dir); + if (r < 0) { + LOG(ERROR, "Failed to close directory handle: %d", r); + } + }); + // Find the latest staged data + char fileName[32] = {}; + int maxSeqNum = 0; + lfs_info entry = {}; + int r = 0; + while ((r = lfs_dir_read(fs, &dir, &entry)) == 1) { + if (entry.type != LFS_TYPE_REG) { + if (entry.type != LFS_TYPE_DIR || (std::strcmp(entry.name, ".") != 0 && std::strcmp(entry.name, "..") != 0)) { + LOG(WARN, "Found unexpected entry in ledger directory"); + } + continue; + } + char* end = nullptr; + int seqNum = std::strtol(entry.name, &end, 10); + if (end != entry.name + std::strlen(entry.name)) { + LOG(WARN, "Found unexpected entry in ledger directory"); + continue; + } + if (seqNum > maxSeqNum) { + if (maxSeqNum > 0) { + // Older staged files must be removed before the most recent file is moved to + // "current", otherwise "current" would get overwritten with outdated data if + // the device resets before the staged directory is cleaned up + CHECK(getStagedFilePath(path, sizeof(path), name_, fileName)); + CHECK_FS(lfs_remove(fs, path)); + } + size_t n = strlcpy(fileName, entry.name, sizeof(fileName)); + if (n >= sizeof(fileName)) { + return SYSTEM_ERROR_INTERNAL; + } + maxSeqNum = seqNum; + } else { + CHECK(getStagedFilePath(path, sizeof(path), name_, entry.name)); + CHECK_FS(lfs_remove(fs, path)); + } + } + CHECK_FS(r); + if (maxSeqNum > 0) { + // Replace "current" with the found file + char destPath[MAX_PATH_LEN + 1]; + CHECK(getCurrentFilePath(destPath, sizeof(destPath), name_)); + CHECK(getStagedFilePath(path, sizeof(path), name_, fileName)); + CHECK_FS(lfs_rename(fs, path, destPath)); + return maxSeqNum; + } + return 0; +} + +int Ledger::removeTempData(lfs_t* fs) { + char path[MAX_PATH_LEN + 1]; + CHECK(getTempDirPath(path, sizeof(path), name_)); + lfs_dir_t dir = {}; + CHECK_FS(lfs_dir_open(fs, &dir, path)); + SCOPE_GUARD({ + int r = closeDir(fs, &dir); + if (r < 0) { + LOG(ERROR, "Failed to close directory handle: %d", r); + } + }); + lfs_info entry = {}; + int r = 0; + while ((r = lfs_dir_read(fs, &dir, &entry)) == 1) { + if (entry.type != LFS_TYPE_REG) { + if (entry.type != LFS_TYPE_DIR || (std::strcmp(entry.name, ".") != 0 && std::strcmp(entry.name, "..") != 0)) { + LOG(WARN, "Found unexpected entry in ledger directory"); + } + continue; + } + CHECK(getTempFilePath(path, sizeof(path), name_, entry.name)); + CHECK_FS(lfs_remove(fs, path)); + } + CHECK_FS(r); + return 0; +} + +void LedgerBase::addRef() const { + LedgerManager::instance()->addLedgerRef(this); +} + +void LedgerBase::release() const { + LedgerManager::instance()->releaseLedger(this); +} + +LedgerInfo& LedgerInfo::update(const LedgerInfo& info) { + if (info.scopeType_.has_value()) { + scopeType_ = info.scopeType_.value(); + } + if (info.scopeId_.has_value()) { + scopeId_ = info.scopeId_.value(); + } + if (info.syncDir_.has_value()) { + syncDir_ = info.syncDir_.value(); + } + if (info.dataSize_.has_value()) { + dataSize_ = info.dataSize_.value(); + } + if (info.lastUpdated_.has_value()) { + lastUpdated_ = info.lastUpdated_.value(); + } + if (info.lastSynced_.has_value()) { + lastSynced_ = info.lastSynced_.value(); + } + if (info.updateCount_.has_value()) { + updateCount_ = info.updateCount_.value(); + } + if (info.syncPending_.has_value()) { + syncPending_ = info.syncPending_.value(); + } + return *this; +} + +int LedgerReader::init(LedgerInfo info, int stagedSeqNum, RefCountPtr ledger) { + char path[MAX_PATH_LEN + 1]; + if (stagedSeqNum > 0) { + // The most recent data is staged + CHECK(getStagedFilePath(path, sizeof(path), ledger->name(), stagedSeqNum)); + staged_ = true; + } else { + CHECK(getCurrentFilePath(path, sizeof(path), ledger->name())); + } + // Open the respective file + FsLock fs; + CHECK_FS(lfs_file_open(fs.instance(), &file_, path, LFS_O_RDONLY)); + ledger_ = std::move(ledger); + info_ = std::move(info); + open_ = true; + return 0; +} + +int LedgerReader::read(char* data, size_t size) { + if (!open_) { + return SYSTEM_ERROR_INVALID_STATE; + } + size_t bytesToRead = std::min(size, info_.dataSize() - dataOffs_); + if (size > 0 && bytesToRead == 0) { + return SYSTEM_ERROR_END_OF_STREAM; + } + FsLock fs; + size_t n = CHECK_FS(lfs_file_read(fs.instance(), &file_, data, bytesToRead)); + if (n != bytesToRead) { + LOG(ERROR, "Unexpected end of ledger data file"); + return SYSTEM_ERROR_LEDGER_INVALID_FORMAT; + } + dataOffs_ += n; + return n; +} + +int LedgerReader::close(bool /* discard */) { + if (!open_) { + return 0; + } + // Consider the reader closed regardless if any of the operations below fails + open_ = false; + FsLock fs; + int result = closeFile(fs.instance(), &file_); + if (result < 0) { + LOG(ERROR, "Error while closing file: %d", result); + } + // Let the ledger flush any data. Avoid holding a lock on the filesystem while trying to acquire + // a lock on the ledger instance + fs.unlock(); + int r = ledger_->notifyReaderClosed(staged_); + fs.lock(); + if (r < 0) { + LOG(ERROR, "Failed to flush ledger data: %d", r); + return r; + } + return result; +} + +int LedgerWriter::init(LedgerWriteSource src, int tempSeqNum, RefCountPtr ledger) { + // Create a temporary file + char path[MAX_PATH_LEN + 1]; + CHECK(getTempFilePath(path, sizeof(path), ledger->name(), tempSeqNum)); + FsLock fs; + CHECK_FS(lfs_file_open(fs.instance(), &file_, path, LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL)); + ledger_ = std::move(ledger); + tempSeqNum_ = tempSeqNum; + src_ = src; + open_ = true; + return 0; +} + +int LedgerWriter::write(const char* data, size_t size) { + if (!open_) { + return SYSTEM_ERROR_INVALID_STATE; + } + if (src_ == LedgerWriteSource::USER && dataSize_ + size > LEDGER_MAX_DATA_SIZE) { + LOG(ERROR, "Ledger data is too long"); + return SYSTEM_ERROR_LEDGER_TOO_LARGE; + } + FsLock fs; + size_t n = CHECK_FS(lfs_file_write(fs.instance(), &file_, data, size)); + if (n != size) { + LOG(ERROR, "Unexpected number of bytes written"); + return SYSTEM_ERROR_FILESYSTEM; + } + dataSize_ += n; + return n; +} + +int LedgerWriter::close(bool discard) { + if (!open_) { + return 0; + } + // Consider the writer closed regardless if any of the operations below fails + open_ = false; + // Lock the ledger instance first then the filesystem, otherwise a deadlock is possible if the + // LedgerManager has locked the ledger instance already but is waiting on the filesystem lock + std::unique_lock lock(*ledger_); + FsLock fs; + if (discard) { + // Remove the temporary file + char path[MAX_PATH_LEN + 1]; + CHECK(getTempFilePath(path, sizeof(path), ledger_->name(), tempSeqNum_)); + int r = closeFile(fs.instance(), &file_); + if (r < 0) { + LOG(ERROR, "Error while closing file: %d", r); + } + CHECK_FS(lfs_remove(fs.instance(), path)); + return r; + } + NAMED_SCOPE_GUARD(removeFileGuard, { + char path[MAX_PATH_LEN + 1]; + int r = getTempFilePath(path, sizeof(path), ledger_->name(), tempSeqNum_); + if (r >= 0) { + r = removeFile(fs.instance(), path); + } + if (r < 0) { + LOG(ERROR, "Failed to remove file: %d", r); + } + }); + NAMED_SCOPE_GUARD(closeFileGuard, { // Will run before removeFileGuard + int r = closeFile(fs.instance(), &file_); + if (r < 0) { + LOG(ERROR, "Error while closing file: %d", r); + } + }); + // Prepare the updated ledger info + auto newInfo = ledger_->info().update(info_); + newInfo.dataSize(dataSize_); // Can't be overridden + newInfo.updateCount(newInfo.updateCount() + 1); // ditto + if (!info_.isLastUpdatedSet()) { + int64_t t = getMillisSinceEpoch(); + if (t < 0) { + if (t != SYSTEM_ERROR_HAL_RTC_INVALID_TIME) { + LOG(ERROR, "Failed to get current time: %d", (int)t); + } + t = 0; // Current time is unknown + } + newInfo.lastUpdated(t); + } + if (src_ == LedgerWriteSource::USER && !info_.isSyncPendingSet()) { + newInfo.syncPending(true); + } + // Write the info section + size_t infoSize = CHECK(writeLedgerInfo(fs.instance(), &file_, ledger_->name(), newInfo)); + // Write the footer + CHECK(writeFooter(fs.instance(), &file_, dataSize_, infoSize)); + closeFileGuard.dismiss(); + CHECK_FS(lfs_file_close(fs.instance(), &file_)); + // Flush the data. Keep the ledger instance locked so that the ledger state is updated atomically + // in the filesystem and RAM. TODO: Finalize writing to the temporary ledger file in Ledger rather + // than in LedgerWriter + int r = ledger_->notifyWriterClosed(newInfo, tempSeqNum_); + if (r < 0) { + LOG(ERROR, "Failed to flush ledger data: %d", r); + return r; + } + removeFileGuard.dismiss(); + if (src_ == LedgerWriteSource::USER) { + // Avoid holding any locks when calling into the manager + fs.unlock(); + lock.unlock(); + LedgerManager::instance()->notifyLedgerChanged(ledger_->syncContext()); + fs.lock(); // FIXME: FsLock doesn't know when it's unlocked + } + return 0; +} + +} // namespace system + +} // namespace particle + +#endif // HAL_PLATFORM_LEDGER diff --git a/system/src/ledger/ledger.h b/system/src/ledger/ledger.h new file mode 100644 index 0000000000..70283e27ec --- /dev/null +++ b/system/src/ledger/ledger.h @@ -0,0 +1,440 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include "hal_platform.h" + +#if HAL_PLATFORM_LEDGER + +#include +#include +#include +#include + +#include "system_ledger.h" + +#include "filesystem.h" +#include "static_recursive_mutex.h" +#include "ref_count.h" +#include "system_error.h" + +namespace particle::system { + +const auto LEDGER_ROOT_DIR = "/usr/ledger"; + +const size_t MAX_LEDGER_SCOPE_ID_SIZE = 32; + +namespace detail { + +class LedgerSyncContext; + +} // namespace detail + +class LedgerManager; +class LedgerReader; +class LedgerWriter; +class LedgerInfo; + +enum class LedgerWriteSource { + USER, + SYSTEM +}; + +struct LedgerScopeId { + char data[MAX_LEDGER_SCOPE_ID_SIZE]; + size_t size; +}; + +const LedgerScopeId EMPTY_LEDGER_SCOPE_ID = {}; + +// The reference counter of a ledger instance is managed by the LedgerManager. We can't safely use a +// regular atomic counter, such as RefCount, because the LedgerManager maintains a list of all created +// ledger instances that needs to be updated when any of the instances is destroyed. Shared pointers +// would work but those can't be used in dynalib interfaces +class LedgerBase { +public: + explicit LedgerBase(detail::LedgerSyncContext* ctx = nullptr) : + syncCtx_(ctx), + refCount_(1) { + } + + LedgerBase(const LedgerBase&) = delete; + + virtual ~LedgerBase() = default; + + void addRef() const; + void release() const; + + LedgerBase& operator=(const LedgerBase&) = delete; + +protected: + detail::LedgerSyncContext* syncContext() const { // Called by LedgerManager and LedgerWriter + return syncCtx_; + } + + int& refCount() const { // Called by LedgerManager + return refCount_; + } + +private: + detail::LedgerSyncContext* syncCtx_; // Sync context + mutable int refCount_; // Reference count + + friend class LedgerManager; + friend class LedgerWriter; +}; + +class Ledger: public LedgerBase { +public: + explicit Ledger(detail::LedgerSyncContext* ctx = nullptr); + ~Ledger(); + + int initReader(LedgerReader& reader); + int initWriter(LedgerWriter& writer, LedgerWriteSource src); + + LedgerInfo info() const; + + const char* name() const { + return name_; // Immutable + } + + void setSyncCallback(ledger_sync_callback callback) { + std::lock_guard lock(*this); + syncCallback_ = callback; + } + + void setAppData(void* data, ledger_destroy_app_data_callback destroy) { + std::lock_guard lock(*this); + appData_ = data; + destroyAppData_ = destroy; + } + + void* appData() const { + std::lock_guard lock(*this); + return appData_; + } + + void lock() const { + mutex_.lock(); + } + + void unlock() const { + mutex_.unlock(); + } + +protected: + int init(const char* name); // Called by LedgerManager + int updateInfo(const LedgerInfo& info); // ditto + void notifySynced(); // ditto + + int notifyReaderClosed(bool staged); // Called by LedgerReader + int notifyWriterClosed(const LedgerInfo& info, int tempSeqNum); // Called by LedgerWriter + +private: + int lastSeqNum_; // Counter incremented every time the ledger is opened for writing + int stagedSeqNum_; // Sequence number assigned to the most recent staged ledger data + int curReaderCount_; // Number of active readers of the current ledger data + int stagedReaderCount_; // Number of active readers of the staged ledger data + int stagedFileCount_; // Number of staged data files created + + int64_t lastUpdated_; // Time the ledger was last time updated + int64_t lastSynced_; // Time the ledger was last synchronized + size_t dataSize_; // Size of the ledger data + unsigned updateCount_; // Counter incremented every time the ledger is updated + bool syncPending_; // Whether the ledger has local changes that have not yet been synchronized + + ledger_sync_callback syncCallback_; // Callback to invoke when the ledger has been synchronized + ledger_destroy_app_data_callback destroyAppData_; // Destructor for the application data + void* appData_; // Application data + + const char* name_; // Ledger name (allocated by LedgerManager) + LedgerScopeId scopeId_; // Scope ID + ledger_scope scopeType_; // Scope type + ledger_sync_direction syncDir_; // Sync direction + + bool inited_; // Whether the ledger is initialized + + mutable StaticRecursiveMutex mutex_; // Ledger lock + + int loadLedgerInfo(lfs_t* fs); + void setLedgerInfo(const LedgerInfo& info); + + int initCurrentData(lfs_t* fs); + int flushStagedData(lfs_t* fs); + int removeTempData(lfs_t* fs); + + friend class LedgerManager; + friend class LedgerReader; + friend class LedgerWriter; +}; + +class LedgerInfo { +public: + LedgerInfo() = default; + + LedgerInfo& scopeType(ledger_scope type) { + scopeType_ = type; + return *this; + } + + ledger_scope scopeType() const { + return scopeType_.value_or(LEDGER_SCOPE_UNKNOWN); + } + + bool isScopeTypeSet() const { + return scopeType_.has_value(); + } + + LedgerInfo& scopeId(LedgerScopeId id) { + scopeId_ = std::move(id); + return *this; + } + + const LedgerScopeId& scopeId() const { + if (!scopeId_.has_value()) { + return EMPTY_LEDGER_SCOPE_ID; + } + return scopeId_.value(); + } + + bool isScopeIdSet() const { + return scopeId_.has_value(); + } + + LedgerInfo& syncDirection(ledger_sync_direction dir) { + syncDir_ = dir; + return *this; + } + + ledger_sync_direction syncDirection() const { + return syncDir_.value_or(LEDGER_SYNC_DIRECTION_UNKNOWN); + } + + bool isSyncDirectionSet() const { + return syncDir_.has_value(); + } + + LedgerInfo& dataSize(size_t size) { + dataSize_ = size; + return *this; + } + + size_t dataSize() const { + return dataSize_.value_or(0); + } + + bool isDataSizeSet() const { + return dataSize_.has_value(); + } + + LedgerInfo& lastUpdated(int64_t time) { + lastUpdated_ = time; + return *this; + } + + int64_t lastUpdated() const { + return lastUpdated_.value_or(0); + } + + bool isLastUpdatedSet() const { + return lastUpdated_.has_value(); + } + + LedgerInfo& lastSynced(int64_t time) { + lastSynced_ = time; + return *this; + } + + int64_t lastSynced() const { + return lastSynced_.value_or(0); + } + + bool isLastSyncedSet() const { + return lastSynced_.has_value(); + } + + LedgerInfo& updateCount(unsigned count) { + updateCount_ = count; + return *this; + } + + unsigned updateCount() const { + return updateCount_.value_or(0); + } + + bool isUpdateCountSet() const { + return updateCount_.has_value(); + } + + LedgerInfo& syncPending(bool pending) { + syncPending_ = pending; + return *this; + } + + bool syncPending() const { + return syncPending_.value_or(false); + } + + bool isSyncPendingSet() const { + return syncPending_.has_value(); + } + + LedgerInfo& update(const LedgerInfo& info); + +private: + // When adding a new field, make sure to update the following methods and functions: + // LedgerInfo::update() + // Ledger::info() + // Ledger::setLedgerInfo() + // Ledger::loadLedgerInfo() + // writeLedgerInfo() + std::optional scopeId_; + std::optional lastUpdated_; + std::optional lastSynced_; + std::optional dataSize_; + std::optional updateCount_; + std::optional scopeType_; + std::optional syncDir_; + std::optional syncPending_; +}; + +class LedgerStream { +public: + virtual ~LedgerStream() = default; + + virtual int read(char* data, size_t size) = 0; + virtual int write(const char* data, size_t size) = 0; + virtual int close(bool discard = false) = 0; +}; + +class LedgerReader: public LedgerStream { +public: + LedgerReader() : + file_(), + dataOffs_(0), + staged_(false), + open_(false) { + } + + // Reader instances are not copyable nor movable + LedgerReader(const LedgerReader&) = delete; + + ~LedgerReader() { + close(true /* discard */); + } + + int read(char* data, size_t size) override; + + int write(const char* data, size_t size) override { + return SYSTEM_ERROR_INVALID_STATE; + } + + int close(bool discard = false) override; + + Ledger* ledger() const { + return ledger_.get(); + } + + const LedgerInfo& info() const { + return info_; + } + + bool isOpen() const { + return open_; + } + + LedgerReader& operator=(const LedgerReader&) = delete; + +protected: + int init(LedgerInfo info, int stagedSeqNum, RefCountPtr ledger); // Called by Ledger + +private: + RefCountPtr ledger_; // Ledger instance + LedgerInfo info_; // Ledger info + lfs_file_t file_; // File handle + size_t dataOffs_; // Current offset in the data section + bool staged_; // Whether the data being read is staged + bool open_; // Whether the reader is open + + friend class Ledger; +}; + +class LedgerWriter: public LedgerStream { +public: + LedgerWriter() : + file_(), + src_(), + dataSize_(0), + tempSeqNum_(0), + open_(false) { + } + + // Writer instances are not copyable nor movable + LedgerWriter(const LedgerWriter&) = delete; + + ~LedgerWriter() { + close(true /* discard */); + } + + int read(char* data, size_t size) override { + return SYSTEM_ERROR_INVALID_STATE; + } + + int write(const char* data, size_t size) override; + int close(bool discard = false) override; + + void updateInfo(const LedgerInfo& info) { + info_.update(info); + } + + Ledger* ledger() const { + return ledger_.get(); + } + + bool isOpen() const { + return open_; + } + + LedgerWriter& operator=(const LedgerWriter&) = delete; + +protected: + int init(LedgerWriteSource src, int tempSeqNum, RefCountPtr ledger); // Called by Ledger + +private: + RefCountPtr ledger_; // Ledger instance + LedgerInfo info_; // Ledger info updates + lfs_file_t file_; // File handle + LedgerWriteSource src_; // Who is writing to the ledger + size_t dataSize_; // Size of the data written + int tempSeqNum_; // Sequence number assigned to the temporary ledger data + bool open_; // Whether the writer is open + + friend class Ledger; +}; + +inline bool operator==(const LedgerScopeId& id1, const LedgerScopeId& id2) { + return id1.size == id2.size && std::memcmp(id1.data, id2.data, id1.size) == 0; +} + +inline bool operator!=(const LedgerScopeId& id1, const LedgerScopeId& id2) { + return !(id1 == id2); +} + +} // namespace particle::system + +#endif // HAL_PLATFORM_LEDGER diff --git a/system/src/ledger/ledger_manager.cpp b/system/src/ledger/ledger_manager.cpp new file mode 100644 index 0000000000..31ca871bc2 --- /dev/null +++ b/system/src/ledger/ledger_manager.cpp @@ -0,0 +1,1604 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#if !defined(DEBUG_BUILD) && !defined(UNIT_TEST) +#define NDEBUG // TODO: Define NDEBUG in release builds +#endif + +#include "hal_platform.h" + +#if HAL_PLATFORM_LEDGER + +#include +#include +#include +#include + +#include + +#include "control/common.h" // FIXME: Move Protobuf utilities to another directory +#include "ledger.h" +#include "ledger_manager.h" +#include "ledger_util.h" +#include "system_ledger.h" +#include "system_cloud.h" + +#include "timer_hal.h" + +#include "nanopb_misc.h" +#include "file_util.h" +#include "time_util.h" +#include "endian_util.h" +#include "scope_guard.h" +#include "check.h" + +#include "cloud/cloud.pb.h" +#include "cloud/ledger.pb.h" + +#define PB_CLOUD(_name) particle_cloud_##_name +#define PB_LEDGER(_name) particle_cloud_ledger_##_name + +static_assert(PB_CLOUD(Response_Result_OK) == 0); // Used by value in the code + +LOG_SOURCE_CATEGORY("system.ledger"); + +namespace particle { + +using control::common::EncodedString; +using fs::FsLock; + +namespace system { + +namespace { + +const unsigned MAX_LEDGER_COUNT = 20; + +const unsigned MIN_SYNC_DELAY = 5000; +const unsigned MAX_SYNC_DELAY = 30000; + +const unsigned MIN_RETRY_DELAY = 30000; +const unsigned MAX_RETRY_DELAY = 5 * 60000; + +const auto REQUEST_URI = "L"; +const auto REQUEST_METHOD = COAP_METHOD_POST; + +const size_t STREAM_BUFFER_SIZE = 128; + +const size_t MAX_PATH_LEN = 127; + +int encodeSetDataRequestPrefix(pb_ostream_t* stream, const char* ledgerName, const LedgerInfo& info) { + // Ledger data may not fit in a single CoAP message. Nanopb streams are synchronous so the + // request is encoded manually + if (!pb_encode_tag(stream, PB_WT_STRING, PB_LEDGER(SetDataRequest_name_tag)) || // name + !pb_encode_string(stream, (const pb_byte_t*)ledgerName, std::strlen(ledgerName))) { + return SYSTEM_ERROR_ENCODING_FAILED; + } + auto& scopeId = info.scopeId(); + if (scopeId.size && (!pb_encode_tag(stream, PB_WT_STRING, PB_LEDGER(SetDataRequest_scope_id_tag)) || // scope_id + !pb_encode_string(stream, (const pb_byte_t*)scopeId.data, scopeId.size))) { + return SYSTEM_ERROR_ENCODING_FAILED; + } + auto lastUpdated = info.lastUpdated(); + if (lastUpdated && (!pb_encode_tag(stream, PB_WT_64BIT, PB_LEDGER(SetDataRequest_last_updated_tag)) || // last_updated + !pb_encode_fixed64(stream, &lastUpdated))) { + return SYSTEM_ERROR_ENCODING_FAILED; + } + auto dataSize = info.dataSize(); + // Encode only the tag and size of the data field. The data itself is encoded by the calling code + if (dataSize && (!pb_encode_tag(stream, PB_WT_STRING, PB_LEDGER(SetDataRequest_data_tag)) || // data + !pb_encode_varint(stream, dataSize))) { + return SYSTEM_ERROR_ENCODING_FAILED; + } + return 0; +} + +int getStreamForSubmessage(coap_message* msg, pb_istream_t* stream, uint32_t fieldTag) { + pb_istream_t s = {}; + CHECK(pb_istream_from_coap_message(&s, msg, nullptr)); + for (;;) { + uint32_t tag = 0; + auto type = pb_wire_type_t(); + bool eof = false; + if (!pb_decode_tag(&s, &type, &tag, &eof)) { + if (eof) { + *stream = s; // Return an empty stream + return 0; + } + return SYSTEM_ERROR_BAD_DATA; + } + if (tag == fieldTag) { + if (type != PB_WT_STRING) { + return SYSTEM_ERROR_BAD_DATA; + } + // Can't use pb_make_string_substream() as the message may contain incomplete data + uint32_t size = 0; + if (!pb_decode_varint32(&s, &size)) { + return SYSTEM_ERROR_BAD_DATA; + } + if (s.bytes_left > size) { + s.bytes_left = size; + } + *stream = s; + return 0; + } + if (!pb_skip_field(&s, type)) { + return SYSTEM_ERROR_BAD_DATA; + } + } +} + +// Returns true if the result code returned by the server indicates that the ledger is or may no +// longer be accessible by the device +inline bool isLedgerAccessError(int result) { + return result == PB_CLOUD(Response_Result_LEDGER_NOT_FOUND) || + result == PB_CLOUD(Response_Result_LEDGER_INVALID_SYNC_DIRECTION) || + result == PB_CLOUD(Response_Result_LEDGER_SCOPE_CHANGED); +} + +inline int closeDir(lfs_t* fs, lfs_dir_t* dir) { // Transforms the LittleFS error to a system error + CHECK_FS(lfs_dir_close(fs, dir)); + return 0; +} + +} // namespace + +namespace detail { + +struct LedgerSyncContext { + char name[LEDGER_MAX_NAME_LENGTH + 1]; // Ledger name + LedgerScopeId scopeId; // Scope ID + Ledger* instance; // Ledger instance. If null, the ledger is not instantiated + ledger_sync_direction syncDir; // Sync direction + int getInfoCount; // Number of GET_INFO requests sent for this ledger + int pendingState; // Pending state flags (LedgerManager::PendingState) + bool syncPending; // Whether the ledger needs to be synchronized + bool taskRunning; // Whether an asynchronous task is running for this ledger + union { + struct { // Fields specific to a device-to-cloud ledger or a ledger with unknown sync direction + uint64_t syncTime; // Time when the ledger should be synchronized (ticks) + uint64_t forcedSyncTime; // The latest time when the ledger should be synchronized (ticks) + uint64_t updateTime; // Time the ledger was last updated (ticks) + unsigned updateCount; // Value of the ledger's update counter when the sync started + }; + struct { // Fields specific to a cloud-to-device ledger + uint64_t lastUpdated; // Time the ledger was last updated (Unix time in milliseconds) + }; + }; + + LedgerSyncContext() : + name(), + scopeId(EMPTY_LEDGER_SCOPE_ID), + instance(nullptr), + syncDir(LEDGER_SYNC_DIRECTION_UNKNOWN), + getInfoCount(0), + pendingState(0), + syncPending(false), + taskRunning(false), + syncTime(0), + forcedSyncTime(0), + updateTime(0), + updateCount(0) { + } + + void updateFromLedgerInfo(const LedgerInfo& info) { + if (info.isSyncDirectionSet()) { + syncDir = info.syncDirection(); + } + if (info.isScopeIdSet()) { + scopeId = info.scopeId(); + } + if (info.isSyncPendingSet()) { + syncPending = info.syncPending(); + } + if (info.isLastUpdatedSet() && syncDir == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { + lastUpdated = info.lastUpdated(); + } + } + + void resetDeviceToCloudState() { + syncTime = 0; + forcedSyncTime = 0; + updateTime = 0; + updateCount = 0; + } + + void resetCloudToDeviceState() { + lastUpdated = 0; + } +}; + +} // namespace detail + +LedgerManager::LedgerManager() : + timer_(timerCallback, this), + curCtx_(nullptr), + nextSyncTime_(0), + retryTime_(0), + retryDelay_(0), + bytesInBuf_(0), + state_(State::NEW), + pendingState_(0), + reqId_(COAP_INVALID_REQUEST_ID), + resubscribe_(false) { +} + +LedgerManager::~LedgerManager() { + reset(); + if (state_ != State::NEW) { + coap_remove_request_handler(REQUEST_URI, REQUEST_METHOD, nullptr); + coap_remove_connection_handler(connectionCallback, nullptr); + } +} + +int LedgerManager::init() { + std::lock_guard lock(mutex_); + if (state_ != State::NEW) { + return 0; // Already initialized + } + // Device must not be connected to the cloud + if (spark_cloud_flag_connected()) { + return SYSTEM_ERROR_INVALID_STATE; + } + // TODO: Allow seeking in ledger and CoAP message streams so that an intermediate buffer is not + // needed when streaming ledger data to and from the server + std::unique_ptr buf(new char[STREAM_BUFFER_SIZE]); + if (!buf) { + return SYSTEM_ERROR_NO_MEMORY; + } + // Enumerate local ledgers + LedgerSyncContexts contexts; + FsLock fs; + lfs_dir_t dir = {}; + int r = lfs_dir_open(fs.instance(), &dir, LEDGER_ROOT_DIR); + if (r == 0) { + SCOPE_GUARD({ + int r = closeDir(fs.instance(), &dir); + if (r < 0) { + LOG(ERROR, "Failed to close directory handle: %d", r); + } + }); + lfs_info entry = {}; + while ((r = lfs_dir_read(fs.instance(), &dir, &entry)) == 1) { + if (entry.type != LFS_TYPE_DIR) { + LOG(WARN, "Found unexpected entry in ledger directory"); + continue; + } + if (std::strcmp(entry.name, ".") == 0 || std::strcmp(entry.name, "..") == 0) { + continue; + } + if (contexts.size() >= (int)MAX_LEDGER_COUNT) { + LOG(ERROR, "Maximum number of ledgers reached, skipping ledger: %s", entry.name); + continue; + } + // Load the ledger info + Ledger ledger; + int r = ledger.init(entry.name); + if (r < 0) { + LOG(ERROR, "Failed to initialize ledger: %d", r); + continue; + } + // Create a sync context for the ledger + std::unique_ptr ctx(new(std::nothrow) LedgerSyncContext()); + if (!ctx) { + return SYSTEM_ERROR_NO_MEMORY; + } + size_t n = strlcpy(ctx->name, entry.name, sizeof(ctx->name)); + if (n >= sizeof(ctx->name)) { + return SYSTEM_ERROR_INTERNAL; // Name length is validated in Ledger::init() + } + auto info = ledger.info(); + ctx->scopeId = info.scopeId(); + ctx->syncDir = info.syncDirection(); + ctx->syncPending = info.syncPending(); + if (ctx->syncDir == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { + ctx->lastUpdated = info.lastUpdated(); + } + // Context objects are sorted by ledger name + bool found = false; + auto it = findContext(contexts, ctx->name, found); + assert(!found); + it = contexts.insert(it, std::move(ctx)); + if (it == contexts.end()) { + return SYSTEM_ERROR_NO_MEMORY; + } + } + CHECK_FS(r); + } else if (r != LFS_ERR_NOENT) { + CHECK_FS(r); // Forward the error + } + CHECK(coap_add_connection_handler(connectionCallback, this, nullptr /* reserved */)); + NAMED_SCOPE_GUARD(removeConnHandler, { + coap_remove_connection_handler(connectionCallback, nullptr /* reserved */); + }); + CHECK(coap_add_request_handler(REQUEST_URI, REQUEST_METHOD, 0 /* flags */, requestCallback, this, nullptr /* reserved */)); + removeConnHandler.dismiss(); + contexts_ = std::move(contexts); + buf_ = std::move(buf); + state_ = State::OFFLINE; + return 0; +} + +int LedgerManager::getLedger(RefCountPtr& ledger, const char* name, bool create) { + std::lock_guard lock(mutex_); + if (state_ == State::NEW) { + return SYSTEM_ERROR_INVALID_STATE; + } + // Check if the requested ledger is already instantiated + bool found = false; + auto it = findContext(name, found); + if (found && (*it)->instance) { + ledger = (*it)->instance; + return 0; + } + std::unique_ptr newCtx; + LedgerSyncContext* ctx = nullptr; + if (!found) { + if (!create) { + return SYSTEM_ERROR_LEDGER_NOT_FOUND; + } + if (contexts_.size() >= (int)MAX_LEDGER_COUNT) { + LOG(ERROR, "Maximum number of ledgers reached"); + return SYSTEM_ERROR_LEDGER_TOO_MANY; + } + // Create a new sync context + newCtx.reset(new(std::nothrow) LedgerSyncContext()); + if (!newCtx) { + return SYSTEM_ERROR_NO_MEMORY; + } + size_t n = strlcpy(newCtx->name, name, sizeof(newCtx->name)); + if (n >= sizeof(newCtx->name)) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + ctx = newCtx.get(); + } else { + ctx = it->get(); + } + // Create a ledger instance + auto lr = makeRefCountPtr(ctx); + if (!lr) { + return SYSTEM_ERROR_NO_MEMORY; + } + int r = lr->init(ctx->name); + if (r < 0) { + LOG(ERROR, "Failed to initialize ledger: %d", r); + return r; + } + if (!found) { + it = contexts_.insert(it, std::move(newCtx)); + if (it == contexts_.end()) { + return SYSTEM_ERROR_NO_MEMORY; + } + if (state_ >= State::READY) { + // Request the info about the newly created ledger + setPendingState(ctx, PendingState::GET_INFO); + runNext(); + } + } + ctx->instance = lr.get(); + ledger = std::move(lr); + return 0; +} + +int LedgerManager::getLedgerNames(Vector& namesArg) { + std::lock_guard lock(mutex_); + if (state_ == State::NEW) { + return SYSTEM_ERROR_INVALID_STATE; + } + // Return all ledgers found in the filesystem, not just the usable ones + FsLock fs; + Vector names; + lfs_dir_t dir = {}; + int r = lfs_dir_open(fs.instance(), &dir, LEDGER_ROOT_DIR); + if (r == 0) { + SCOPE_GUARD({ + int r = closeDir(fs.instance(), &dir); + if (r < 0) { + LOG(ERROR, "Failed to close directory handle: %d", r); + } + }); + lfs_info entry = {}; + while ((r = lfs_dir_read(fs.instance(), &dir, &entry)) == 1) { + if (entry.type != LFS_TYPE_DIR) { + LOG(WARN, "Found unexpected entry in ledger directory"); + continue; + } + if (std::strcmp(entry.name, ".") == 0 || std::strcmp(entry.name, "..") == 0) { + continue; + } + CString name(entry.name); + if (!name || !names.append(std::move(name))) { + return SYSTEM_ERROR_NO_MEMORY; + } + } + CHECK_FS(r); + } else if (r != LFS_ERR_NOENT) { + CHECK_FS(r); // Forward the error + } + namesArg = std::move(names); + return 0; +} + +int LedgerManager::removeLedgerData(const char* name) { + if (!*name) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + std::lock_guard lock(mutex_); + // TODO: Allow removing ledgers regardless of the device state or if the given ledger is in use + if (state_ != State::OFFLINE) { + return SYSTEM_ERROR_INVALID_STATE; + } + bool found = false; + auto it = findContext(name, found); + if (found) { + if ((*it)->instance) { + return SYSTEM_ERROR_LEDGER_IN_USE; + } + contexts_.erase(it); + } + char path[MAX_PATH_LEN + 1]; + CHECK(formatLedgerPath(path, sizeof(path), name)); + CHECK(rmrf(path)); + return 0; +} + +int LedgerManager::removeAllData() { + std::lock_guard lock(mutex_); + // TODO: Allow removing ledgers regardless of the device state or if the given ledger is in use + if (state_ != State::OFFLINE) { + return SYSTEM_ERROR_INVALID_STATE; + } + for (auto& ctx: contexts_) { + if (ctx->instance) { + return SYSTEM_ERROR_LEDGER_IN_USE; + } + } + contexts_.clear(); + CHECK(rmrf(LEDGER_ROOT_DIR)); + return 0; +} + +int LedgerManager::run() { + auto now = hal_timer_millis(nullptr); + if (state_ == State::FAILED) { + if (now >= retryTime_) { + LOG(INFO, "Retrying synchronization"); + startSync(); + return 1; // Have a task to run + } + runNext(retryTime_ - now); + return 0; + } + if (state_ != State::READY || (!pendingState_ && !resubscribe_)) { + // Some task is in progress, the manager is not in an appropriate state, or there's nothing to do + return 0; + } + if (pendingState_ & PendingState::GET_INFO) { + LOG(INFO, "Requesting ledger info"); + CHECK(sendGetInfoRequest()); + return 1; + } + if ((pendingState_ & PendingState::SUBSCRIBE) || resubscribe_) { + LOG(INFO, "Subscribing to ledger updates"); + CHECK(sendSubscribeRequest()); + return 1; + } + unsigned nextSyncDelay = 0; + if (pendingState_ & PendingState::SYNC_TO_CLOUD) { + if (now >= nextSyncTime_) { + uint64_t t = 0; + LedgerSyncContext* ctx = nullptr; + for (auto& c: contexts_) { + if (c->pendingState & PendingState::SYNC_TO_CLOUD) { + if (now >= c->syncTime) { + ctx = c.get(); + break; + } + if (!t || c->syncTime < t) { + t = c->syncTime; + } + } + } + if (ctx) { + LOG(TRACE, "Synchronizing ledger: %s", ctx->name); + CHECK(sendSetDataRequest(ctx)); + return 1; + } + // Timestamp in nextSyncTime_ was outdated + nextSyncTime_ = t; + } + nextSyncDelay = nextSyncTime_ - now; + } + if (pendingState_ & PendingState::SYNC_FROM_CLOUD) { + LedgerSyncContext* ctx = nullptr; + for (auto& c: contexts_) { + if (c->pendingState & PendingState::SYNC_FROM_CLOUD) { + ctx = c.get(); + break; + } + } + if (ctx) { + LOG(TRACE, "Synchronizing ledger: %s", ctx->name); + CHECK(sendGetDataRequest(ctx)); + return 1; + } + } + if (nextSyncDelay > 0) { + runNext(nextSyncDelay); + } + return 0; +} + +int LedgerManager::notifyConnected() { + if (state_ != State::OFFLINE) { + return SYSTEM_ERROR_INVALID_STATE; + } + LOG(TRACE, "Connected"); + startSync(); + return 0; +} + +void LedgerManager::notifyDisconnected(int /* error */) { + if (state_ == State::OFFLINE) { + return; + } + if (state_ == State::NEW) { + LOG_DEBUG(ERROR, "Unexpected manager state: %d", (int)state_); + return; + } + LOG(TRACE, "Disconnected"); + reset(); + retryDelay_ = 0; + state_ = State::OFFLINE; +} + +int LedgerManager::receiveRequest(CoapMessagePtr& msg, int reqId) { + if (state_ < State::READY) { + return SYSTEM_ERROR_INVALID_STATE; + } + // Get the request type. XXX: It's assumed that the message fields are encoded in order of their + // field numbers, which is not guaranteed by the Protobuf spec in general + char buf[32] = {}; + size_t n = CHECK(coap_peek_payload(msg.get(), buf, sizeof(buf), nullptr)); + pb_istream_t stream = pb_istream_from_buffer((const pb_byte_t*)buf, n); + uint32_t reqType = 0; + uint32_t fieldTag = 0; + auto fieldType = pb_wire_type_t(); + bool eof = false; + if (!pb_decode_tag(&stream, &fieldType, &fieldTag, &eof)) { + if (!eof) { + return SYSTEM_ERROR_BAD_DATA; + } + // Request type field is missing so its value is 0 + } else if (fieldTag == PB_CLOUD(Request_type_tag)) { + if (fieldType != PB_WT_VARINT || !pb_decode_varint32(&stream, &reqType)) { + return SYSTEM_ERROR_BAD_DATA; + } + } // else: Request type field is missing so its value is 0 + switch (reqType) { + case PB_CLOUD(Request_Type_LEDGER_NOTIFY_UPDATE): { + CHECK(receiveNotifyUpdateRequest(msg, reqId)); + break; + } + case PB_CLOUD(Request_Type_LEDGER_RESET_INFO): { + CHECK(receiveResetInfoRequest(msg, reqId)); + break; + } + default: + LOG(ERROR, "Unknown request type: %d", (int)reqType); + return SYSTEM_ERROR_NOT_SUPPORTED; + } + return 0; +} + +int LedgerManager::receiveNotifyUpdateRequest(CoapMessagePtr& msg, int /* reqId */) { + LOG(TRACE, "Received update notification"); + pb_istream_t stream = {}; + CHECK(getStreamForSubmessage(msg.get(), &stream, PB_CLOUD(Request_ledger_notify_update_tag))); + PB_LEDGER(NotifyUpdateRequest) pbReq = {}; + pbReq.ledgers.arg = this; + pbReq.ledgers.funcs.decode = [](pb_istream_t* stream, const pb_field_iter_t* /* field */, void** arg) { + auto self = (LedgerManager*)*arg; + PB_LEDGER(NotifyUpdateRequest_Ledger) pbLedger = {}; + if (!pb_decode(stream, &PB_LEDGER(NotifyUpdateRequest_Ledger_msg), &pbLedger)) { + return false; + } + // Get the context of the updated ledger + bool found = false; + auto it = self->findContext(pbLedger.name, found); + if (!found) { + LOG(WARN, "Unknown ledger: %s", pbLedger.name); + return true; // Ignore + } + auto ctx = it->get(); + if (ctx->syncDir != LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { + if (ctx->syncDir == LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD) { + LOG(ERROR, "Received update notification for device-to-cloud ledger: %s", ctx->name); + } else { + LOG(ERROR, "Received update notification for ledger with unknown sync direction: %s", ctx->name); + } + self->setPendingState(ctx, PendingState::GET_INFO); + return true; + } + if (pbLedger.last_updated > ctx->lastUpdated) { + // Schedule a sync for the updated ledger + LOG(TRACE, "Ledger changed: %s", ctx->name); + self->setPendingState(ctx, PendingState::SYNC_FROM_CLOUD); + } + return true; + }; + if (!pb_decode(&stream, &PB_LEDGER(NotifyUpdateRequest_msg), &pbReq)) { + return SYSTEM_ERROR_BAD_DATA; + } + return 0; +} + +int LedgerManager::receiveResetInfoRequest(CoapMessagePtr& /* msg */, int /* reqId */) { + LOG(WARN, "Received a reset request, re-requesting ledger info"); + for (auto& ctx: contexts_) { + setPendingState(ctx.get(), PendingState::GET_INFO); + } + return 0; +} + +int LedgerManager::receiveResponse(CoapMessagePtr& msg, int status) { + assert(state_ > State::READY); + auto codeClass = COAP_CODE_CLASS(status); + if (codeClass != 2 && codeClass != 4) { // Success 2.xx or Client Error 4.xx + LOG(ERROR, "Ledger request failed: %d.%02d", (int)codeClass, (int)COAP_CODE_DETAIL(status)); + return SYSTEM_ERROR_LEDGER_REQUEST_FAILED; + } + // Get the protocol-specific result code. XXX: It's assumed that the message fields are encoded + // in order of their field numbers, which is not guaranteed by the Protobuf spec in general + char buf[32] = {}; + int r = coap_peek_payload(msg.get(), buf, sizeof(buf), nullptr); + if (r < 0) { + if (r != SYSTEM_ERROR_END_OF_STREAM) { + return r; + } + r = 0; // Response is empty + } + pb_istream_t stream = pb_istream_from_buffer((const pb_byte_t*)buf, r); + int64_t result = 0; + uint32_t fieldTag = 0; + auto fieldType = pb_wire_type_t(); + bool eof = false; + if (!pb_decode_tag(&stream, &fieldType, &fieldTag, &eof)) { + if (!eof) { + return SYSTEM_ERROR_BAD_DATA; + } + // Result code field is missing so its value is 0 + } else if (fieldTag == PB_CLOUD(Response_result_tag)) { + if (fieldType != PB_WT_VARINT || !pb_decode_svarint(&stream, &result)) { + return SYSTEM_ERROR_BAD_DATA; + } + } // else: Result code field is missing so its value is 0 + if (codeClass != 2) { + if (result == PB_CLOUD(Response_Result_OK)) { + // CoAP response code indicates an error but the protocol-specific result code is OK + return SYSTEM_ERROR_LEDGER_INVALID_RESPONSE; + } + // This is not necessarily a critical error + LOG(WARN, "Ledger request failed: %d.%02d", (int)codeClass, (int)COAP_CODE_DETAIL(status)); + } + switch (state_) { + case State::SYNC_TO_CLOUD: { + CHECK(receiveSetDataResponse(msg, result)); + break; + } + case State::SYNC_FROM_CLOUD: { + CHECK(receiveGetDataResponse(msg, result)); + break; + } + case State::SUBSCRIBE: { + CHECK(receiveSubscribeResponse(msg, result)); + break; + } + case State::GET_INFO: { + CHECK(receiveGetInfoResponse(msg, result)); + break; + } + default: + LOG(ERROR, "Unexpected response"); + return SYSTEM_ERROR_INTERNAL; + } + return 0; +} + +int LedgerManager::receiveSetDataResponse(CoapMessagePtr& /* msg */, int result) { + assert(state_ == State::SYNC_TO_CLOUD && curCtx_ && curCtx_->syncDir == LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD && + curCtx_->taskRunning); + if (result == 0) { + LOG(TRACE, "Sent ledger data: %s", curCtx_->name); + LedgerInfo newInfo; + auto now = CHECK(getMillisSinceEpoch()); + newInfo.lastSynced(now); + RefCountPtr ledger; + CHECK(getLedger(ledger, curCtx_->name)); + // Make sure the ledger can't be changed while we're updating its persistently stored state + // and sync context + std::unique_lock ledgerLock(*ledger); + curCtx_->syncTime = 0; + curCtx_->forcedSyncTime = 0; + if (ledger->info().updateCount() == curCtx_->updateCount) { + newInfo.syncPending(false); + curCtx_->syncPending = false; + } else { + // Ledger changed while being synchronized + assert(curCtx_->pendingState & PendingState::SYNC_TO_CLOUD); + updateSyncTime(curCtx_); + } + CHECK(ledger->updateInfo(newInfo)); + ledgerLock.unlock(); + ledger->notifySynced(); // TODO: Invoke asynchronously + // TODO: Reorder the ledger entries so that they're synchronized in a round-robin fashion + } else { + LOG(ERROR, "Failed to sync ledger: %s; result: %d", curCtx_->name, result); + if (!isLedgerAccessError(result)) { + return SYSTEM_ERROR_LEDGER_REQUEST_FAILED; + } + // Ledger may no longer be accessible, re-request its info + LOG(WARN, "Re-requesting ledger info: %s", curCtx_->name); + setPendingState(curCtx_, PendingState::GET_INFO | PendingState::SYNC_TO_CLOUD); + } + curCtx_->taskRunning = false; + curCtx_ = nullptr; + state_ = State::READY; + return 0; +} + +int LedgerManager::receiveGetDataResponse(CoapMessagePtr& msg, int result) { + assert(state_ == State::SYNC_FROM_CLOUD && curCtx_ && curCtx_->syncDir == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE && + curCtx_->taskRunning && !stream_ && !msg_); + if (result != 0) { + LOG(ERROR, "Failed to sync ledger: %s; result: %d", curCtx_->name, result); + if (!isLedgerAccessError(result)) { + return SYSTEM_ERROR_LEDGER_REQUEST_FAILED; + } + // Ledger may no longer be accessible, re-request its info + LOG(WARN, "Re-requesting ledger info: %s", curCtx_->name); + setPendingState(curCtx_, PendingState::GET_INFO | PendingState::SYNC_FROM_CLOUD); + curCtx_->taskRunning = false; + curCtx_ = nullptr; + state_ = State::READY; + return 0; + } + LOG(TRACE, "Received ledger data: %s", curCtx_->name); + // Open the ledger for writing + RefCountPtr ledger; + CHECK(getLedger(ledger, curCtx_->name)); + std::unique_ptr writer(new(std::nothrow) LedgerWriter()); + CHECK(ledger->initWriter(*writer, LedgerWriteSource::SYSTEM)); + // Ledger data may span multiple CoAP messages. Nanopb streams are synchronous so the response + // is decoded manually + pb_istream_t pbStream = {}; + CHECK(getStreamForSubmessage(msg.get(), &pbStream, PB_CLOUD(Response_ledger_get_data_tag))); + uint64_t lastUpdated = 0; + for (;;) { + uint32_t fieldTag = 0; + auto fieldType = pb_wire_type_t(); + bool eof = false; + if (!pb_decode_tag(&pbStream, &fieldType, &fieldTag, &eof)) { + if (!eof) { + return SYSTEM_ERROR_BAD_DATA; + } + break; + } + if (fieldTag == PB_LEDGER(GetDataResponse_last_updated_tag)) { + if (fieldType != PB_WT_64BIT || !pb_decode_fixed64(&pbStream, &lastUpdated)) { + return SYSTEM_ERROR_BAD_DATA; + } + } else if (fieldTag == PB_LEDGER(GetDataResponse_data_tag)) { + if (!pb_skip_field(&pbStream, PB_WT_VARINT)) { // Skip the field length + return SYSTEM_ERROR_BAD_DATA; + } + // "data" is always the last field in the message. XXX: It's assumed that the message + // fields are encoded in order of their field numbers, which is not guaranteed by the + // Protobuf spec in general + break; + } + } + writer->updateInfo(LedgerInfo().lastUpdated(lastUpdated)); + stream_.reset(writer.release()); + msg_ = std::move(msg); + // Read the first chunk of the ledger data + CHECK(receiveLedgerData()); + return 0; +} + +int LedgerManager::receiveSubscribeResponse(CoapMessagePtr& msg, int result) { + assert(state_ == State::SUBSCRIBE); + if (result != 0) { + LOG(ERROR, "Failed to subscribe to ledger updates; result: %d", result); + if (!isLedgerAccessError(result)) { + return SYSTEM_ERROR_LEDGER_REQUEST_FAILED; + } + // Some of the ledgers may no longer be accessible, re-request the info for the ledgers we + // tried to subscribe to + for (auto& ctx: contexts_) { + if (ctx->taskRunning) { + LOG(WARN, "Re-requesting ledger info: %s", ctx->name); + setPendingState(ctx.get(), PendingState::GET_INFO | PendingState::SUBSCRIBE); + ctx->taskRunning = false; + } + } + state_ = State::READY; + return 0; + } + LOG(INFO, "Subscribed to ledger updates"); + PB_LEDGER(SubscribeResponse) pbResp = {}; + struct DecodeContext { + LedgerManager* self; + int error; + }; + DecodeContext d = { .self = this, .error = 0 }; + pbResp.ledgers.arg = &d; + pbResp.ledgers.funcs.decode = [](pb_istream_t* stream, const pb_field_iter_t* /* field */, void** arg) { + auto d = (DecodeContext*)*arg; + PB_LEDGER(SubscribeResponse_Ledger) pbLedger = {}; + if (!pb_decode(stream, &PB_LEDGER(SubscribeResponse_Ledger_msg), &pbLedger)) { + return false; + } + LOG(TRACE, "Subscribed to ledger updates: %s", pbLedger.name); + bool found = false; + auto it = d->self->findContext(pbLedger.name, found); + if (!found) { + LOG(WARN, "Unknown ledger: %s", pbLedger.name); + return true; // Ignore + } + auto ctx = it->get(); + if (!ctx->taskRunning) { + LOG(ERROR, "Unexpected subscription: %s", ctx->name); + d->error = SYSTEM_ERROR_LEDGER_INVALID_RESPONSE; + return false; + } + ctx->taskRunning = false; + if ((pbLedger.has_last_updated && pbLedger.last_updated > ctx->lastUpdated) || ctx->syncPending) { + d->self->setPendingState(ctx, PendingState::SYNC_FROM_CLOUD); + } + return true; + }; + pb_istream_t stream = {}; + CHECK(getStreamForSubmessage(msg.get(), &stream, PB_CLOUD(Response_ledger_subscribe_tag))); + if (!pb_decode(&stream, &PB_LEDGER(SubscribeResponse_msg), &pbResp)) { + return (d.error < 0) ? d.error : SYSTEM_ERROR_BAD_DATA; + } + for (auto& ctx: contexts_) { + if (ctx->taskRunning) { + LOG(ERROR, "Missing subscription: %s", ctx->name); + return SYSTEM_ERROR_LEDGER_INVALID_RESPONSE; + } + } + resubscribe_ = false; + state_ = State::READY; + return 0; +} + +int LedgerManager::receiveGetInfoResponse(CoapMessagePtr& msg, int result) { + assert(state_ == State::GET_INFO); + if (result != 0) { + LOG(ERROR, "Failed to get ledger info; result: %d", result); + return SYSTEM_ERROR_LEDGER_REQUEST_FAILED; + } + LOG(INFO, "Received ledger info"); + struct DecodeContext { + LedgerManager* self; + int error; + bool resubscribe; + bool localInfoIsInvalid; + }; + DecodeContext d = { .self = this, .error = 0, .resubscribe = false, .localInfoIsInvalid = false }; + PB_LEDGER(GetInfoResponse) pbResp = {}; + pbResp.ledgers.arg = &d; + pbResp.ledgers.funcs.decode = [](pb_istream_t* stream, const pb_field_iter_t* /* field */, void** arg) { + auto d = (DecodeContext*)*arg; + PB_LEDGER(GetInfoResponse_Ledger) pbLedger = {}; + if (!pb_decode(stream, &PB_LEDGER(GetInfoResponse_Ledger_msg), &pbLedger)) { + return false; + } + LOG(TRACE, "Received ledger info: name: %s; sync direction: %d; scope type: %d;", pbLedger.name, + (int)pbLedger.sync_direction, (int)pbLedger.scope_type); + LOG_PRINT(TRACE, "scope ID: "); + LOG_DUMP(TRACE, pbLedger.scope_id.bytes, pbLedger.scope_id.size); + LOG_PRINT(TRACE, "\r\n"); + RefCountPtr ledger; + d->error = d->self->getLedger(ledger, pbLedger.name); + if (d->error < 0) { + return false; + } + auto ctx = ledger->syncContext(); + assert(ctx); + if (!ctx->taskRunning) { + LOG(ERROR, "Received unexpected ledger info: %s", ctx->name); + d->error = SYSTEM_ERROR_LEDGER_INVALID_RESPONSE; + return false; + } + ctx->taskRunning = false; + LedgerInfo newInfo; + newInfo.syncDirection(static_cast(pbLedger.sync_direction)); + newInfo.scopeType(static_cast(pbLedger.scope_type)); + if (newInfo.syncDirection() == LEDGER_SYNC_DIRECTION_UNKNOWN || newInfo.scopeType() == LEDGER_SCOPE_UNKNOWN) { + LOG(ERROR, "Received ledger info has invalid scope type or sync direction: %s", pbLedger.name); + d->error = SYSTEM_ERROR_LEDGER_INVALID_RESPONSE; + return false; + } + LedgerScopeId remoteScopeId = {}; + if (pbLedger.scope_id.size > sizeof(remoteScopeId.data)) { + d->error = SYSTEM_ERROR_INTERNAL; // This should have been validated by nanopb + return false; + } + std::memcpy(remoteScopeId.data, pbLedger.scope_id.bytes, pbLedger.scope_id.size); + remoteScopeId.size = pbLedger.scope_id.size; + newInfo.scopeId(remoteScopeId); + auto localInfo = ledger->info(); + auto& localScopeId = localInfo.scopeId(); + bool scopeIdChanged = localScopeId != remoteScopeId; + if (scopeIdChanged || localInfo.syncDirection() != newInfo.syncDirection() || localInfo.scopeType() != newInfo.scopeType()) { + if (localInfo.syncDirection() != LEDGER_SYNC_DIRECTION_UNKNOWN) { + if (scopeIdChanged) { + // Device was likely moved to another product which happens to have a ledger + // with the same name + LOG(WARN, "Ledger scope changed: %s", ctx->name); + d->self->clearPendingState(ctx, ctx->pendingState); + if (newInfo.syncDirection() == LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD) { + // Do not sync this ledger until it's updated again + newInfo.syncPending(false); + if (localInfo.syncDirection() == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { + ctx->resetDeviceToCloudState(); + d->resubscribe = true; + } + } else { + assert(newInfo.syncDirection() == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE); + // Ignore the timestamps when synchronizing this ledger + newInfo.syncPending(true); + if (localInfo.syncDirection() == LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD) { + ctx->resetCloudToDeviceState(); + } + d->resubscribe = true; + } + } else { // DEVICE_TO_CLOUD -> CLOUD_TO_DEVICE or vice versa + // This should not normally happen as the sync direction and scope type of an + // existing ledger cannot be changed + LOG(ERROR, "Ledger scope type or sync direction changed: %s", ctx->name); + newInfo.syncDirection(LEDGER_SYNC_DIRECTION_UNKNOWN); + newInfo.scopeType(LEDGER_SCOPE_UNKNOWN); + if (ctx->syncDir == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { + ctx->resetDeviceToCloudState(); + } + d->localInfoIsInvalid = true; // Will cause a transition to the failed state + } + } else if (newInfo.syncDirection() == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { // UNKNOWN -> CLOUD_TO_DEVICE + if (localInfo.syncPending()) { + LOG(WARN, "Ledger has local changes but its actual sync direction is cloud-to-device: %s", ctx->name); + // Ignore the timestamps when synchronizing this ledger + newInfo.syncPending(true); + } + ctx->resetCloudToDeviceState(); + d->resubscribe = true; + } else if (localInfo.syncPending()) { // UNKNOWN -> DEVICE_TO_CLOUD + d->self->setPendingState(ctx, PendingState::SYNC_TO_CLOUD); + d->self->updateSyncTime(ctx); + } + // Save the new ledger info + d->error = ledger->updateInfo(newInfo); + if (d->error < 0) { + return false; + } + ctx->updateFromLedgerInfo(newInfo); + } + return true; + }; + pb_istream_t stream = {}; + CHECK(getStreamForSubmessage(msg.get(), &stream, PB_CLOUD(Response_ledger_get_info_tag))); + if (!pb_decode(&stream, &PB_LEDGER(GetInfoResponse_msg), &pbResp)) { + return (d.error < 0) ? d.error : SYSTEM_ERROR_BAD_DATA; + } + for (auto& ctx: contexts_) { + if (ctx->taskRunning) { + // Ledger doesn't exist or is no longer accessible by the device + LOG(WARN, "Ledger not found: %s", ctx->name); + if (ctx->syncDir != LEDGER_SYNC_DIRECTION_UNKNOWN) { // DEVICE_TO_CLOUD/CLOUD_TO_DEVICE -> UNKNOWN + LedgerInfo info; + info.syncDirection(LEDGER_SYNC_DIRECTION_UNKNOWN); + info.scopeType(LEDGER_SCOPE_UNKNOWN); + RefCountPtr ledger; + CHECK(getLedger(ledger, ctx->name)); + CHECK(ledger->updateInfo(info)); + if (ctx->syncDir == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { + ctx->resetDeviceToCloudState(); + d.resubscribe = true; + } + ctx->updateFromLedgerInfo(info); + } + clearPendingState(ctx.get(), ctx->pendingState); + ctx->taskRunning = false; + } else if (d.resubscribe && ctx->syncDir == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE) { + setPendingState(ctx.get(), PendingState::SUBSCRIBE); + } + } + if (d.resubscribe) { + // Make sure to clear the subscriptions on the server if no ledgers left to subscribe to. + // TODO: Reuse pendingState_ for storing a state not associated with a particular sync context + resubscribe_ = true; + } + if (d.localInfoIsInvalid) { + return SYSTEM_ERROR_LEDGER_INCONSISTENT_STATE; + } + state_ = State::READY; + return 0; +} + +int LedgerManager::sendSetDataRequest(LedgerSyncContext* ctx) { + assert(state_ == State::READY && (ctx->pendingState & PendingState::SYNC_TO_CLOUD) && + ctx->syncDir == LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD && !curCtx_ && !stream_ && !msg_); + // Open the ledger for reading + RefCountPtr ledger; + CHECK(getLedger(ledger, ctx->name)); + std::unique_ptr reader(new(std::nothrow) LedgerReader()); + CHECK(ledger->initReader(*reader)); + auto info = reader->info(); + // Create a request message + coap_message* apiMsg = nullptr; + int reqId = CHECK(coap_begin_request(&apiMsg, REQUEST_URI, REQUEST_METHOD, 0 /* timeout */, 0 /* flags */, nullptr /* reserved */)); + CoapMessagePtr msg(apiMsg); + // Calculate the size of the request's submessage (particle.cloud.ledger.SetDataRequest) + pb_ostream_t pbStream = PB_OSTREAM_SIZING; + CHECK(encodeSetDataRequestPrefix(&pbStream, ctx->name, info)); + size_t submsgSize = pbStream.bytes_written + info.dataSize(); + // Encode the outer request message (particle.cloud.Request) + CHECK(pb_ostream_from_coap_message(&pbStream, msg.get(), nullptr)); + if (!pb_encode_tag(&pbStream, PB_WT_VARINT, PB_CLOUD(Request_type_tag)) || // type + !pb_encode_varint(&pbStream, PB_CLOUD(Request_Type_LEDGER_SET_DATA))) { + return SYSTEM_ERROR_ENCODING_FAILED; + } + if (!pb_encode_tag(&pbStream, PB_WT_STRING, PB_CLOUD(Request_ledger_set_data_tag)) || // ledger_set_data + !pb_encode_varint(&pbStream, submsgSize)) { + return SYSTEM_ERROR_ENCODING_FAILED; + } + CHECK(encodeSetDataRequestPrefix(&pbStream, ctx->name, info)); + // Encode and send the first chunk of the ledger data + stream_.reset(reader.release()); + reqId_ = reqId; + msg_ = std::move(msg); + CHECK(sendLedgerData()); + // Clear the pending state + clearPendingState(ctx, PendingState::SYNC_TO_CLOUD); + ctx->updateCount = info.updateCount(); + ctx->taskRunning = true; + curCtx_ = ctx; + state_ = State::SYNC_TO_CLOUD; + return 0; +} + +int LedgerManager::sendGetDataRequest(LedgerSyncContext* ctx) { + assert(state_ == State::READY && (ctx->pendingState & PendingState::SYNC_FROM_CLOUD) && + ctx->syncDir == LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE && !curCtx_); + // Prepare a request message + PB_CLOUD(Request) pbReq = {}; + pbReq.type = PB_CLOUD(Request_Type_LEDGER_GET_DATA); + pbReq.which_data = PB_CLOUD(Request_ledger_get_data_tag); + size_t n = strlcpy(pbReq.data.ledger_get_data.name, ctx->name, sizeof(pbReq.data.ledger_get_data.name)); + if (n >= sizeof(pbReq.data.ledger_get_data.name)) { + return SYSTEM_ERROR_INTERNAL; + } + if (ctx->scopeId.size > sizeof(pbReq.data.ledger_get_data.scope_id.bytes)) { + return SYSTEM_ERROR_INTERNAL; + } + std::memcpy(pbReq.data.ledger_get_data.scope_id.bytes, ctx->scopeId.data, ctx->scopeId.size); + pbReq.data.ledger_get_data.scope_id.size = ctx->scopeId.size; + // Send the request + coap_message* apiMsg = nullptr; + int reqId = CHECK(coap_begin_request(&apiMsg, REQUEST_URI, REQUEST_METHOD, 0 /* timeout */, 0 /* flags */, nullptr /* reserved */)); + CoapMessagePtr msg(apiMsg); + pb_ostream_t stream = {}; + CHECK(pb_ostream_from_coap_message(&stream, msg.get(), nullptr)); + if (!pb_encode(&stream, &PB_CLOUD(Request_msg), &pbReq)) { + return SYSTEM_ERROR_ENCODING_FAILED; + } + CHECK(coap_end_request(msg.get(), responseCallback, nullptr /* ack_cb */, requestErrorCallback, this, nullptr)); + msg.release(); + // Clear the pending state + clearPendingState(ctx, PendingState::SYNC_FROM_CLOUD); + ctx->taskRunning = true; + curCtx_ = ctx; + reqId_ = reqId; + state_ = State::SYNC_FROM_CLOUD; + return 0; +} + +int LedgerManager::sendSubscribeRequest() { + assert(state_ == State::READY && (pendingState_ & PendingState::SUBSCRIBE)); + struct EncodeContext { + LedgerManager* self; + int error; + }; + EncodeContext d = { .self = this, .error = 0 }; + PB_CLOUD(Request) pbReq = {}; + pbReq.type = PB_CLOUD(Request_Type_LEDGER_SUBSCRIBE); + pbReq.which_data = PB_CLOUD(Request_ledger_subscribe_tag); + pbReq.data.ledger_subscribe.ledgers.arg = &d; + pbReq.data.ledger_subscribe.ledgers.funcs.encode = [](pb_ostream_t* stream, const pb_field_iter_t* field, void* const* arg) { + // Make sure not to update any state in this callback as it may be called multiple times + auto d = (EncodeContext*)*arg; + for (auto& ctx: d->self->contexts_) { + if (ctx->pendingState & PendingState::SUBSCRIBE) { + PB_LEDGER(SubscribeRequest_Ledger) pbLedger = {}; + size_t n = strlcpy(pbLedger.name, ctx->name, sizeof(pbLedger.name)); + if (n >= sizeof(pbLedger.name) || ctx->scopeId.size > sizeof(pbLedger.scope_id.bytes)) { + d->error = SYSTEM_ERROR_INTERNAL; + return false; + } + std::memcpy(pbLedger.scope_id.bytes, ctx->scopeId.data, ctx->scopeId.size); + pbLedger.scope_id.size = ctx->scopeId.size; + if (!pb_encode_tag_for_field(stream, field) || + !pb_encode_submessage(stream, &PB_LEDGER(SubscribeRequest_Ledger_msg), &pbLedger)) { + return false; + } + } + } + return true; + }; + coap_message* apiMsg = nullptr; + int reqId = CHECK(coap_begin_request(&apiMsg, REQUEST_URI, REQUEST_METHOD, 0 /* timeout */, 0 /* flags */, nullptr /* reserved */)); + CoapMessagePtr msg(apiMsg); + pb_ostream_t stream = {}; + CHECK(pb_ostream_from_coap_message(&stream, msg.get(), nullptr)); + if (!pb_encode(&stream, &PB_CLOUD(Request_msg), &pbReq)) { + return (d.error < 0) ? d.error : SYSTEM_ERROR_ENCODING_FAILED; + } + CHECK(coap_end_request(msg.get(), responseCallback, nullptr /* ack_cb */, requestErrorCallback, this, nullptr)); + msg.release(); + // Clear the pending state + pendingState_ = 0; + for (auto& ctx: contexts_) { + if (ctx->pendingState & PendingState::SUBSCRIBE) { + ctx->pendingState &= ~PendingState::SUBSCRIBE; + ctx->taskRunning = true; + } + pendingState_ |= ctx->pendingState; + } + reqId_ = reqId; + state_ = State::SUBSCRIBE; + return 0; +} + +int LedgerManager::sendGetInfoRequest() { + assert(state_ == State::READY && (pendingState_ & PendingState::GET_INFO)); + struct EncodeContext { + LedgerManager* self; + int error; + }; + EncodeContext d = { .self = this, .error = 0 }; + PB_CLOUD(Request) pbReq = {}; + pbReq.type = PB_CLOUD(Request_Type_LEDGER_GET_INFO); + pbReq.which_data = PB_CLOUD(Request_ledger_get_info_tag); + pbReq.data.ledger_get_info.ledgers.arg = &d; + pbReq.data.ledger_get_info.ledgers.funcs.encode = [](pb_ostream_t* stream, const pb_field_iter_t* field, void* const* arg) { + // Make sure not to update any state in this callback as it may be called multiple times + auto d = (EncodeContext*)*arg; + for (auto& ctx: d->self->contexts_) { + if (ctx->pendingState & PendingState::GET_INFO) { + // This is to prevent sending GET_INFO requests in a loop if a subsequent SET_DATA or + // SUBSCRIBE request keeps failing with a result code that triggers another GET_INFO + // request. This can only happen due to a server error + if (ctx->getInfoCount >= 10) { + LOG(ERROR, "Sent too many info requests for ledger: %s", ctx->name); + d->error = SYSTEM_ERROR_LEDGER_INCONSISTENT_STATE; + return false; + } + if (!pb_encode_tag_for_field(stream, field) || + !pb_encode_string(stream, (const pb_byte_t*)ctx->name, std::strlen(ctx->name))) { + return false; + } + } + } + return true; + }; + coap_message* apiMsg = nullptr; + int reqId = CHECK(coap_begin_request(&apiMsg, REQUEST_URI, REQUEST_METHOD, 0 /* timeout */, 0 /* flags */, nullptr /* reserved */)); + CoapMessagePtr msg(apiMsg); + pb_ostream_t stream = {}; + CHECK(pb_ostream_from_coap_message(&stream, msg.get(), nullptr)); + if (!pb_encode(&stream, &PB_CLOUD(Request_msg), &pbReq)) { + return (d.error < 0) ? d.error : SYSTEM_ERROR_ENCODING_FAILED; + } + CHECK(coap_end_request(msg.get(), responseCallback, nullptr /* ack_cb */, requestErrorCallback, this, nullptr)); + msg.release(); + // Clear the pending state + pendingState_ = 0; + for (auto& ctx: contexts_) { + if (ctx->pendingState & PendingState::GET_INFO) { + ctx->pendingState &= ~PendingState::GET_INFO; + ++ctx->getInfoCount; + ctx->taskRunning = true; + } + pendingState_ |= ctx->pendingState; + } + reqId_ = reqId; + state_ = State::GET_INFO; + return 0; +} + +int LedgerManager::sendLedgerData() { + assert(stream_ && msg_); + bool eof = false; + for (;;) { + if (bytesInBuf_ > 0) { + size_t size = bytesInBuf_; + int r = CHECK(coap_write_payload(msg_.get(), buf_.get(), &size, messageBlockCallback, requestErrorCallback, this, nullptr)); + if (r == COAP_RESULT_WAIT_BLOCK) { + assert(size < bytesInBuf_); + bytesInBuf_ -= size; + std::memmove(buf_.get(), buf_.get() + size, bytesInBuf_); + LOG_DEBUG(TRACE, "Waiting current block of ledger data to be sent"); + break; + } + assert(size == bytesInBuf_); + bytesInBuf_ = 0; + } + int r = stream_->read(buf_.get(), STREAM_BUFFER_SIZE); + if (r < 0) { + if (r == SYSTEM_ERROR_END_OF_STREAM) { + eof = true; + break; + } + return r; + } + assert(r != 0); + bytesInBuf_ = r; + } + if (eof) { + CHECK(stream_->close()); + stream_.reset(); + CHECK(coap_end_request(msg_.get(), responseCallback, nullptr /* ack_cb */, requestErrorCallback, this, nullptr)); + msg_.release(); + } + return 0; +} + +int LedgerManager::receiveLedgerData() { + assert(curCtx_ && stream_ && msg_); + bool eof = false; + for (;;) { + if (bytesInBuf_ > 0) { + CHECK(stream_->write(buf_.get(), bytesInBuf_)); + bytesInBuf_ = 0; + } + size_t size = STREAM_BUFFER_SIZE; + int r = coap_read_payload(msg_.get(), buf_.get(), &size, messageBlockCallback, requestErrorCallback, this, nullptr); + if (r < 0) { + if (r == SYSTEM_ERROR_END_OF_STREAM) { + eof = true; + break; + } + return r; + } + bytesInBuf_ = size; + if (r == COAP_RESULT_WAIT_BLOCK) { + LOG_DEBUG(TRACE, "Waiting next block of ledger data to be received"); + break; + } + } + if (eof) { + auto now = CHECK(getMillisSinceEpoch()); + auto writer = static_cast(stream_.get()); + LedgerInfo info; + info.lastSynced(now); + info.syncPending(false); + writer->updateInfo(info); + RefCountPtr ledger(writer->ledger()); + CHECK(stream_->close()); + stream_.reset(); + msg_.reset(); + reqId_ = COAP_INVALID_REQUEST_ID; + curCtx_->taskRunning = false; + curCtx_ = nullptr; + state_ = State::READY; + ledger->notifySynced(); // TODO: Invoke asynchronously + } + return 0; +} + +int LedgerManager::sendResponse(int result, int reqId) { + int code = (result == 0) ? COAP_STATUS_CHANGED : COAP_STATUS_BAD_REQUEST; + coap_message* apiMsg = nullptr; + CHECK(coap_begin_response(&apiMsg, code, reqId, 0 /* flags */, nullptr /* reserved */)); + CoapMessagePtr msg(apiMsg); + PB_CLOUD(Response) pbResp = {}; + pbResp.result = result; + EncodedString pbMsg(&pbResp.message); + if (result < 0) { + pbMsg.data = get_system_error_message(result); + pbMsg.size = std::strlen(pbMsg.data); + } + CHECK(coap_end_response(msg.get(), nullptr /* ack_cb */, requestErrorCallback, nullptr /* arg */, nullptr /* reserved */)); + msg.release(); + return 0; +} + +void LedgerManager::setPendingState(LedgerSyncContext* ctx, int state) { + ctx->pendingState |= state; + pendingState_ |= state; +} + +void LedgerManager::clearPendingState(LedgerSyncContext* ctx, int state) { + ctx->pendingState &= ~state; + pendingState_ = 0; + for (auto& ctx: contexts_) { + pendingState_ |= ctx->pendingState; + } +} + +void LedgerManager::updateSyncTime(LedgerSyncContext* ctx, bool changed) { + // TODO: Revisit the throttling algorithm + auto now = hal_timer_millis(nullptr); + bool throttle = ctx->updateTime && now - ctx->updateTime < MIN_SYNC_DELAY; + if (changed) { + ctx->updateTime = now; + } + if (throttle) { + if (!ctx->forcedSyncTime) { + ctx->forcedSyncTime = now + MAX_SYNC_DELAY; + } + ctx->syncTime = std::min(now + MIN_SYNC_DELAY, ctx->forcedSyncTime); + } else { + // Synchronize the first update in series right away + ctx->syncTime = now; + ctx->forcedSyncTime = now; + } + if (!nextSyncTime_ || ctx->syncTime < nextSyncTime_) { + nextSyncTime_ = ctx->syncTime; + } +} + +void LedgerManager::startSync() { + assert(state_ == State::OFFLINE || state_ == State::FAILED); + for (auto& ctx: contexts_) { + switch (ctx->syncDir) { + case LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD: { + if (ctx->syncPending) { + setPendingState(ctx.get(), PendingState::SYNC_TO_CLOUD); + updateSyncTime(ctx.get()); + } + break; + } + case LEDGER_SYNC_DIRECTION_CLOUD_TO_DEVICE: { + setPendingState(ctx.get(), PendingState::SUBSCRIBE); + break; + } + case LEDGER_SYNC_DIRECTION_UNKNOWN: { + setPendingState(ctx.get(), PendingState::GET_INFO); + break; + } + default: + break; + } + } + state_ = State::READY; +} + +void LedgerManager::reset() { + timer_.stop(); + if (reqId_ != COAP_INVALID_REQUEST_ID) { + coap_cancel_request(reqId_, nullptr); + reqId_ = COAP_INVALID_REQUEST_ID; + } + msg_.reset(); + if (stream_) { + int r = stream_->close(true /* discard */); + if (r < 0) { + LOG(ERROR, "Failed to close ledger stream: %d", r); + } + stream_.reset(); + } + for (auto& ctx: contexts_) { + if (ctx->syncDir == LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD || ctx->syncDir == LEDGER_SYNC_DIRECTION_UNKNOWN) { + ctx->syncTime = 0; + ctx->forcedSyncTime = 0; + } + ctx->getInfoCount = 0; + ctx->pendingState = 0; + ctx->taskRunning = false; + } + pendingState_ = 0; + nextSyncTime_ = 0; + bytesInBuf_ = 0; + curCtx_ = nullptr; + // retryDelay_ is reset when disconnecting from the cloud +} + +void LedgerManager::runNext(unsigned delay) { + int r = timer_.start(delay); + if (r < 0) { + LOG(ERROR, "Failed to start timer: %d", r); + } +} + +void LedgerManager::handleError(int error) { + assert(error < 0); + if (state_ >= State::READY) { + retryDelay_ = std::clamp(retryDelay_ * 2, MIN_RETRY_DELAY, MAX_RETRY_DELAY); + LOG(ERROR, "Synchronization failed: %d; retrying in %us", error, retryDelay_ / 1000); + reset(); + retryTime_ = hal_timer_millis(nullptr) + retryDelay_; + state_ = State::FAILED; + } +} + +LedgerManager::LedgerSyncContexts::ConstIterator LedgerManager::findContext(const LedgerSyncContexts& contexts, const char* name, bool& found) { + found = false; + auto it = std::lower_bound(contexts.begin(), contexts.end(), name, [&found](const auto& ctx, const char* name) { + auto r = std::strcmp(ctx->name, name); + if (r == 0) { + found = true; + } + return r < 0; + }); + return it; +} + +void LedgerManager::notifyLedgerChanged(LedgerSyncContext* ctx) { + std::lock_guard lock(mutex_); + if (ctx->syncDir == LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD || ctx->syncDir == LEDGER_SYNC_DIRECTION_UNKNOWN) { + // Mark the ledger as changed but only schedule a sync for it if its actual sync direction is known + ctx->syncPending = true; + if (ctx->syncDir != LEDGER_SYNC_DIRECTION_UNKNOWN && state_ >= State::READY) { + setPendingState(ctx, PendingState::SYNC_TO_CLOUD); + updateSyncTime(ctx, true /* changed */); + runNext(); + } + } +} + +void LedgerManager::addLedgerRef(const LedgerBase* ledger) { + std::lock_guard lock(mutex_); + ++ledger->refCount(); +} + +void LedgerManager::releaseLedger(const LedgerBase* ledger) { + std::lock_guard lock(mutex_); + if (--ledger->refCount() == 0) { + auto ctx = ledger->syncContext(); + ctx->instance = nullptr; + delete ledger; + } +} + +int LedgerManager::connectionCallback(int error, int status, void* arg) { + auto self = static_cast(arg); + std::lock_guard lock(self->mutex_); + int r = 0; + switch (status) { + case COAP_CONNECTION_OPEN: { + r = self->notifyConnected(); + break; + } + case COAP_CONNECTION_CLOSED: { + self->notifyDisconnected(error); + break; + } + default: + break; + } + if (r < 0) { + LOG(ERROR, "Failed to handle connection status change: %d", error); + self->handleError(r); + } + // Make sure to run any asynchronous tasks that might be pending whenever the manager is + // notified about some event + self->runNext(); + return 0; +} + +int LedgerManager::requestCallback(coap_message* apiMsg, const char* uri, int method, int reqId, void* arg) { + auto self = static_cast(arg); + std::lock_guard lock(self->mutex_); + clear_system_error_message(); + CoapMessagePtr msg(apiMsg); + int result = self->receiveRequest(msg, reqId); + if (result < 0) { + LOG(ERROR, "Error while handling request: %d", result); + } + int r = self->sendResponse(result, reqId); + if (r < 0 && r != SYSTEM_ERROR_COAP_REQUEST_NOT_FOUND) { // Response might have been sent already + LOG(ERROR, "Failed to send response: %d", r); + if (result >= 0) { + result = r; + } + } + if (result < 0) { + self->handleError(result); + } + self->runNext(); + return 0; +} + +int LedgerManager::responseCallback(coap_message* apiMsg, int status, int reqId, void* arg) { + auto self = static_cast(arg); + std::lock_guard lock(self->mutex_); + CoapMessagePtr msg(apiMsg); + int r = self->receiveResponse(msg, status); + if (r < 0) { + LOG(ERROR, "Error while handling response: %d", r); + self->handleError(r); + } + self->runNext(); + return 0; +} + +int LedgerManager::messageBlockCallback(coap_message* msg, int reqId, void* arg) { + auto self = static_cast(arg); + std::lock_guard lock(self->mutex_); + assert(self->msg_.get() == msg && self->reqId_ == reqId); + int r = 0; + if (self->state_ == State::SYNC_TO_CLOUD) { + r = self->sendLedgerData(); + } else if (self->state_ == State::SYNC_FROM_CLOUD) { + r = self->receiveLedgerData(); + } else { + LOG(ERROR, "Unexpected block message"); + r = SYSTEM_ERROR_INTERNAL; + } + if (r < 0) { + self->handleError(r); + } + self->runNext(); + return 0; +} + +void LedgerManager::requestErrorCallback(int error, int /* reqId */, void* arg) { + auto self = static_cast(arg); + std::lock_guard lock(self->mutex_); + LOG(ERROR, "Request failed: %d", error); + self->handleError(error); + self->runNext(); +} + +void LedgerManager::timerCallback(void* arg) { + auto self = static_cast(arg); + std::lock_guard lock(self->mutex_); + int r = self->run(); + if (r < 0) { + LOG(ERROR, "Failed to process ledger task: %d", r); + self->handleError(r); + } + if (r != 0) { + // Keep running until there's nothing to do + self->runNext(); + } +} + +LedgerManager* LedgerManager::instance() { + static LedgerManager mgr; + // XXX: Lazy initialization is used so that ledger instances can be requested in the global + // scope by the application. It's dangerous because the application's global constructors are + // called before the system is fully initialized but seems to work in this case + static volatile bool initCalled = false; + if (!initCalled) { + int r = mgr.init(); + initCalled = true; + if (r < 0) { + LOG(ERROR, "Failed to initialize ledger manager: %d", r); + } + } + return &mgr; +} + +} // namespace system + +} // namespace particle + +#endif // HAL_PLATFORM_LEDGER diff --git a/system/src/ledger/ledger_manager.h b/system/src/ledger/ledger_manager.h new file mode 100644 index 0000000000..3f8691df7d --- /dev/null +++ b/system/src/ledger/ledger_manager.h @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include "hal_platform.h" + +#if HAL_PLATFORM_LEDGER + +#include +#include + +#include "util/system_timer.h" + +#include "coap_api.h" +#include "coap_util.h" + +#include "c_string.h" +#include "static_recursive_mutex.h" +#include "ref_count.h" + +#include "spark_wiring_vector.h" + +namespace particle::system { + +namespace detail { + +class LedgerSyncContext; + +} // namespace detail + +class Ledger; +class LedgerBase; +class LedgerWriter; +class LedgerStream; + +class LedgerManager { +public: + LedgerManager(const LedgerManager&) = delete; + + ~LedgerManager(); + + int getLedger(RefCountPtr& ledger, const char* name, bool create = false); + + int getLedgerNames(Vector& names); + + int removeLedgerData(const char* name); + int removeAllData(); + + LedgerManager& operator=(const LedgerManager&) = delete; + + static LedgerManager* instance(); + +protected: + using LedgerSyncContext = detail::LedgerSyncContext; + + void notifyLedgerChanged(LedgerSyncContext* ctx); // Called by LedgerWriter + + void addLedgerRef(const LedgerBase* ledger); // Called by LedgerBase + void releaseLedger(const LedgerBase* ledger); // ditto + +private: + enum class State { + NEW, // Manager is not initialized + OFFLINE, // Device is offline + FAILED, // Synchronization failed (device is online) + READY, // Ready to run a task + SYNC_TO_CLOUD, // Synchronizing a device-to-cloud ledger + SYNC_FROM_CLOUD, // Synchronizing a cloud-to-device ledger + SUBSCRIBE, // Subscribing to ledger updates + GET_INFO // Getting ledger info + }; + + enum PendingState { + SYNC_TO_CLOUD = 0x01, // Synchronization of a device-to-cloud ledger is pending + SYNC_FROM_CLOUD = 0x02, // Synchronization of a cloud-to-device ledger is pending + SUBSCRIBE = 0x04, // Subscription to updates is pending + GET_INFO = 0x08 // Ledger info is missing + }; + + typedef Vector> LedgerSyncContexts; + + LedgerSyncContexts contexts_; // Preallocated context objects for all known ledgers + std::unique_ptr stream_; // Input or output stream open for the ledger being synchronized + std::unique_ptr buf_; // Intermediate buffer used for piping ledger data + SystemTimer timer_; // Timer used for running asynchronous tasks + CoapMessagePtr msg_; // CoAP request or response that is being sent or received + LedgerSyncContext* curCtx_; // Context of the ledger being synchronized + uint64_t nextSyncTime_; // Time when the next device-to-cloud ledger needs to be synchronized (ticks) + uint64_t retryTime_; // Time when synchronization can be retried (ticks) + unsigned retryDelay_; // Delay before retrying synchronization + size_t bytesInBuf_; // Number of bytes stored in the intermediate buffer + State state_; // Current manager state + int pendingState_; // Pending ledger state flags + int reqId_; // ID of the ongoing CoAP request + bool resubscribe_; // Whether the ledger subcriptions need to be updated + + mutable StaticRecursiveMutex mutex_; // Manager lock + + LedgerManager(); // Use LedgerManager::instance() + + int init(); + + int run(); + + int notifyConnected(); + void notifyDisconnected(int error); + + int receiveRequest(CoapMessagePtr& msg, int reqId); + int receiveNotifyUpdateRequest(CoapMessagePtr& msg, int reqId); + int receiveResetInfoRequest(CoapMessagePtr& msg, int reqId); + + int receiveResponse(CoapMessagePtr& msg, int status); + int receiveSetDataResponse(CoapMessagePtr& msg, int result); + int receiveGetDataResponse(CoapMessagePtr& msg, int result); + int receiveSubscribeResponse(CoapMessagePtr& msg, int result); + int receiveGetInfoResponse(CoapMessagePtr& msg, int result); + + int sendSetDataRequest(LedgerSyncContext* ctx); + int sendGetDataRequest(LedgerSyncContext* ctx); + int sendSubscribeRequest(); + int sendGetInfoRequest(); + + int sendLedgerData(); + int receiveLedgerData(); + + int sendResponse(int result, int reqId); + + void setPendingState(LedgerSyncContext* ctx, int state); + void clearPendingState(LedgerSyncContext* ctx, int state); + void updateSyncTime(LedgerSyncContext* ctx, bool changed = false); + + void startSync(); + void reset(); + + void runNext(unsigned delay = 0); + + void handleError(int error); + + LedgerSyncContexts::ConstIterator findContext(const char* name, bool& found) const { + return findContext(contexts_, name, found); + } + + static LedgerSyncContexts::ConstIterator findContext(const LedgerSyncContexts& contexts, const char* name, bool& found); + + static int connectionCallback(int error, int status, void* arg); + static int requestCallback(coap_message* msg, const char* uri, int method, int reqId, void* arg); + static int responseCallback(coap_message* msg, int status, int reqId, void* arg); + static int messageBlockCallback(coap_message* msg, int reqId, void* arg); + static void requestErrorCallback(int error, int reqId, void* arg); + static void timerCallback(void* arg); + + friend class LedgerWriter; + friend class LedgerBase; +}; + +} // namespace particle::system + +#endif // HAL_PLATFORM_LEDGER diff --git a/system/src/ledger/ledger_util.cpp b/system/src/ledger/ledger_util.cpp new file mode 100644 index 0000000000..4bfb96165d --- /dev/null +++ b/system/src/ledger/ledger_util.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#include "hal_platform.h" + +#if HAL_PLATFORM_LEDGER + +#include +#include + +#include "ledger.h" +#include "ledger_util.h" + +#include "system_error.h" + +namespace particle::system { + +int formatLedgerPath(char* buf, size_t size, const char* ledgerName, const char* fmt, ...) { + // Format the prefix part of the path + int n = std::snprintf(buf, size, "%s/%s/", LEDGER_ROOT_DIR, ledgerName); + if (n < 0) { + return SYSTEM_ERROR_INTERNAL; + } + size_t pos = n; + if (pos >= size) { + return SYSTEM_ERROR_PATH_TOO_LONG; + } + if (fmt) { + // Format the rest of the path + va_list args; + va_start(args, fmt); + n = vsnprintf(buf + pos, size - pos, fmt, args); + va_end(args); + if (n < 0) { + return SYSTEM_ERROR_INTERNAL; + } + pos += n; + if (pos >= size) { + return SYSTEM_ERROR_PATH_TOO_LONG; + } + } + return pos; +} + +} // namespace particle::system + +#endif // HAL_PLATFORM_LEDGER diff --git a/communication/src/coap_util.h b/system/src/ledger/ledger_util.h similarity index 57% rename from communication/src/coap_util.h rename to system/src/ledger/ledger_util.h index a9858d613a..7476b13af9 100644 --- a/communication/src/coap_util.h +++ b/system/src/ledger/ledger_util.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Particle Industries, Inc. All rights reserved. + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -17,19 +17,16 @@ #pragma once -#include "logging.h" +#include "hal_platform.h" -namespace particle::protocol { +#if HAL_PLATFORM_LEDGER -/** - * Log the contents of a CoAP message. - * - * @param level Logging level. - * @param category Logging category. - * @param data Message data. - * @param size Message size. - * @param logPayload Whether to log the payload data of the message. - */ -void logCoapMessage(LogLevel level, const char* category, const char* data, size_t size, bool logPayload = false); +#include + +namespace particle::system { + +int formatLedgerPath(char* buf, size_t size, const char* ledgerName, const char* fmt = nullptr, ...); + +} // namespace particle::system -} // namespace particle::protocol +#endif // HAL_PLATFORM_LEDGER diff --git a/system/src/main.cpp b/system/src/main.cpp index 0503d7a756..bb1f99c3d5 100644 --- a/system/src/main.cpp +++ b/system/src/main.cpp @@ -27,6 +27,10 @@ */ /* Includes ------------------------------------------------------------------*/ + +// STATIC_ASSERT macro clashes with the nRF SDK +#define NO_STATIC_ASSERT + #include "debug.h" #include "system_event.h" #include "system_mode.h" @@ -63,6 +67,8 @@ #include "spark_wiring_wifi.h" #include "server_config.h" #include "system_network_manager.h" +#include "ledger/ledger_manager.h" +#include "ledger/ledger.h" // FIXME #include "system_control_internal.h" @@ -668,7 +674,17 @@ int resetSettingsToFactoryDefaultsIfNeeded() { CHECK(dct_write_app_data(devPubKey.get(), DCT_ALT_DEVICE_PUBLIC_KEY_OFFSET, DCT_ALT_DEVICE_PUBLIC_KEY_SIZE)); // Restore default server key and address ServerConfig::instance()->restoreDefaultSettings(); - particle::system::NetworkManager::instance()->clearStoredConfiguration(); + system::NetworkManager::instance()->clearStoredConfiguration(); + // TODO: Discuss if we'd want to clear ledger data on a factory reset +#if 0 +#if HAL_PLATFORM_LEDGER + // Can't use LedgerManager::removeAllData() because the manager is not initialized yet + CHECK(rmrf(system::LEDGER_ROOT_DIR)); +#endif // HAL_PLATFORM_LEDGER + // XXX: Application global constructors run before this function so an additional system reset + // is performed to prevent any inconsistencies that might be caused by the configuration cleanup + HAL_Core_System_Reset_Ex(RESET_REASON_CONFIG_UPDATE, 0 /* data */, nullptr /* reserved */); +#endif // 0 #endif // !defined(SPARK_NO_PLATFORM) && HAL_PLATFORM_DCT return 0; } @@ -784,6 +800,13 @@ void app_setup_and_loop(void) } Network_Setup(threaded); // todo - why does this come before system thread initialization? +#if HAL_PLATFORM_LEDGER + if (system_mode() != SAFE_MODE) { + // Make sure the ledger manager is initialized + system::LedgerManager::instance(); + } +#endif // HAL_PLATFORM_LEDGER + #if PLATFORM_THREADING if (threaded) { diff --git a/system/src/system_event.cpp b/system/src/system_event.cpp index a58503987c..a84f5e1338 100644 --- a/system/src/system_event.cpp +++ b/system/src/system_event.cpp @@ -30,6 +30,7 @@ namespace { using namespace particle; +using namespace particle::system; struct SystemEventSubscription { @@ -115,7 +116,7 @@ class SystemEventTask : public ISRTaskQueue::Task { */ void notify() { system_notify_event_async(event_, data_, pointer_, fn_, fndata_); - system_pool_free(this, nullptr); + systemPoolDelete(this); } public: @@ -197,9 +198,8 @@ void system_notify_event(system_event_t event, uint32_t data, void* pointer, voi if (flags & NOTIFY_SYNCHRONOUSLY) { system_notify_event_impl(event, data, pointer, fn, fndata); } else if (hal_interrupt_is_isr()) { - void* space = (system_pool_alloc(sizeof(SystemEventTask), nullptr)); - if (space) { - auto task = new (space) SystemEventTask(event, data, pointer, fn, fndata); + auto task = systemPoolNew(event, data, pointer, fn, fndata); + if (task) { SystemISRTaskQueue.enqueue(task); }; } else { diff --git a/system/src/system_ledger.cpp b/system/src/system_ledger.cpp new file mode 100644 index 0000000000..80b77bb141 --- /dev/null +++ b/system/src/system_ledger.cpp @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#include "hal_platform.h" + +#if HAL_PLATFORM_LEDGER + +#include +#include + +#include "ledger/ledger.h" +#include "ledger/ledger_manager.h" +#include "system_ledger.h" +#include "system_threading.h" + +#include "check.h" + +using namespace particle; +using namespace particle::system; + +int ledger_get_instance(ledger_instance** ledger, const char* name, void* reserved) { + RefCountPtr lr; + CHECK(LedgerManager::instance()->getLedger(lr, name, true /* create */)); + *ledger = reinterpret_cast(lr.unwrap()); // Transfer ownership + return 0; +} + +void ledger_add_ref(ledger_instance* ledger, void* reserved) { + auto lr = reinterpret_cast(ledger); + lr->addRef(); +} + +void ledger_release(ledger_instance* ledger, void* reserved) { + if (ledger) { + auto lr = reinterpret_cast(ledger); + lr->release(); + } +} + +void ledger_lock(ledger_instance* ledger, void* reserved) { + auto lr = reinterpret_cast(ledger); + lr->lock(); +} + +void ledger_unlock(ledger_instance* ledger, void* reserved) { + auto lr = reinterpret_cast(ledger); + lr->unlock(); +} + +void ledger_set_callbacks(ledger_instance* ledger, const ledger_callbacks* callbacks, void* reserved) { + auto lr = reinterpret_cast(ledger); + lr->setSyncCallback(callbacks ? callbacks->sync : nullptr); +} + +void ledger_set_app_data(ledger_instance* ledger, void* appData, ledger_destroy_app_data_callback destroy, + void* reserved) { + auto lr = reinterpret_cast(ledger); + lr->setAppData(appData, destroy); +} + +void* ledger_get_app_data(ledger_instance* ledger, void* reserved) { + auto lr = reinterpret_cast(ledger); + return lr->appData(); +} + +int ledger_get_info(ledger_instance* ledger, ledger_info* info, void* reserved) { + auto lr = reinterpret_cast(ledger); + auto srcInfo = lr->info(); + info->name = lr->name(); + info->last_updated = srcInfo.lastUpdated(); + info->last_synced = srcInfo.lastSynced(); + info->data_size = srcInfo.dataSize(); + info->scope = srcInfo.scopeType(); + info->sync_direction = srcInfo.syncDirection(); + info->flags = 0; + if (srcInfo.syncPending()) { + info->flags |= LEDGER_INFO_SYNC_PENDING; + } + return 0; +} + +int ledger_open(ledger_stream** stream, ledger_instance* ledger, int mode, void* reserved) { + auto lr = reinterpret_cast(ledger); + if (mode & LEDGER_STREAM_MODE_READ) { + // Bidirectional streams are not supported as of now + if (mode & LEDGER_STREAM_MODE_WRITE) { + return SYSTEM_ERROR_NOT_SUPPORTED; + } + std::unique_ptr r(new(std::nothrow) LedgerReader()); + if (!r) { + return SYSTEM_ERROR_NO_MEMORY; + } + CHECK(lr->initReader(*r)); + *stream = reinterpret_cast(r.release()); // Transfer ownership + } else if (mode & LEDGER_STREAM_MODE_WRITE) { + if (mode & LEDGER_STREAM_MODE_READ) { + return SYSTEM_ERROR_NOT_SUPPORTED; + } + std::unique_ptr w(new(std::nothrow) LedgerWriter()); + if (!w) { + return SYSTEM_ERROR_NO_MEMORY; + } + CHECK(lr->initWriter(*w, LedgerWriteSource::USER)); + *stream = reinterpret_cast(w.release()); + } else { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + return 0; +} + +int ledger_close(ledger_stream* stream, int flags, void* reserved) { + if (!stream) { + return 0; + } + auto s = reinterpret_cast(stream); + int r = s->close(flags & LEDGER_STREAM_CLOSE_DISCARD); + delete s; + return r; +} + +int ledger_read(ledger_stream* stream, char* data, size_t size, void* reserved) { + auto s = reinterpret_cast(stream); + size_t n = CHECK(s->read(data, size)); + return n; +} + +int ledger_write(ledger_stream* stream, const char* data, size_t size, void* reserved) { + auto s = reinterpret_cast(stream); + size_t n = CHECK(s->write(data, size)); + return n; +} + +int ledger_get_names(char*** names, size_t* count, void* reserved) { + Vector namesVec; + CHECK(LedgerManager::instance()->getLedgerNames(namesVec)); + *names = (char**)std::malloc(sizeof(char*) * namesVec.size()); + if (!*names && namesVec.size() > 0) { + return SYSTEM_ERROR_NO_MEMORY; + } + for (int i = 0; i < namesVec.size(); ++i) { + (*names)[i] = namesVec[i].unwrap(); // Transfer ownership + } + *count = namesVec.size(); + return 0; +} + +int ledger_purge(const char* name, void* reserved) { + CHECK(LedgerManager::instance()->removeLedgerData(name)); + return 0; +} + +int ledger_purge_all(void* reserved) { + CHECK(LedgerManager::instance()->removeAllData()); + return 0; +} + +#endif // HAL_PLATFORM_LEDGER diff --git a/system/src/system_listening_mode.cpp b/system/src/system_listening_mode.cpp index 0315b25d2d..7e8a7ddcce 100644 --- a/system/src/system_listening_mode.cpp +++ b/system/src/system_listening_mode.cpp @@ -172,7 +172,7 @@ int ListeningModeHandler::command(network_listen_command_t com, void* arg) { } int ListeningModeHandler::enqueueCommand(network_listen_command_t com, void* arg) { - auto task = static_cast(system_pool_alloc(sizeof(Task), nullptr)); + auto task = systemPoolNew(); if (!task) { return SYSTEM_ERROR_NO_MEMORY; } @@ -198,7 +198,7 @@ void ListeningModeHandler::executeEnqueuedCommand(Task* task) { auto com = task->command; auto arg = task->arg; - system_pool_free(task, nullptr); + systemPoolDelete(task); instance()->command(com, arg); } diff --git a/system/src/system_network_manager.cpp b/system/src/system_network_manager.cpp index 1aad086841..c787e0de74 100644 --- a/system/src/system_network_manager.cpp +++ b/system/src/system_network_manager.cpp @@ -1237,7 +1237,7 @@ int NetworkManager::loadStoredConfiguration(spark::Vector& bool r = conf->append(storedConf); return r; }; - const int r = decodeMessageFromFile(&file, PB(NetworkConfig_fields), &pbConf); + const int r = decodeProtobufFromFile(&file, PB(NetworkConfig_fields), &pbConf); if (r < 0) { LOG(ERROR, "Unable to parse network settings"); LOG(WARN, "Removing file: %s", NETWORK_CONFIG_FILE); @@ -1320,7 +1320,7 @@ int NetworkManager::saveStoredConfiguration(const spark::Vectoropen(); CloudDiagnostics::instance()->status(CloudDiagnostics::CONNECTED); system_notify_event(cloud_status, cloud_status_connected); if (system_mode() == SAFE_MODE) { @@ -476,7 +479,7 @@ int system_isr_task_queue_free_memory(void *ptrToFree) { void *arg; }; - auto task = static_cast(system_pool_alloc(sizeof(FreeTask), nullptr)); + auto task = systemPoolNew(); if (!task) { return SYSTEM_ERROR_NO_MEMORY; } @@ -485,7 +488,7 @@ int system_isr_task_queue_free_memory(void *ptrToFree) { task->func = [](ISRTaskQueue::Task * task) { auto freeTask = reinterpret_cast(task); free(freeTask->arg); - system_pool_free(task, nullptr); + systemPoolDelete(freeTask); }; SystemISRTaskQueue.enqueue(task); diff --git a/system/src/usb_control_request_channel.cpp b/system/src/usb_control_request_channel.cpp index 85b13b3bf4..a624fc1f00 100644 --- a/system/src/usb_control_request_channel.cpp +++ b/system/src/usb_control_request_channel.cpp @@ -40,6 +40,7 @@ namespace { using namespace particle; +using namespace particle::system; // Minimum length of the data stage for high-speed USB devices const size_t MIN_WLENGTH = 64; @@ -298,7 +299,7 @@ bool particle::UsbControlRequestChannel::processInitRequest(HAL_USB_SetupRequest return ServiceReply().status(ServiceReply::BUSY).encode(halReq); // Too many active requests } // Allocate a request object from the pool - const auto req = (Request*)system_pool_alloc(sizeof(Request), nullptr); + const auto req = systemPoolNew(); if (!req) { return ServiceReply().status(ServiceReply::NO_MEMORY).encode(halReq); // Memory allocation error } @@ -572,7 +573,7 @@ void particle::UsbControlRequestChannel::finishActiveRequest(Request* req) { req->request_data = nullptr; } if (!req->request_data && !req->reply_data && !req->handler) { - system_pool_free(req, nullptr); + systemPoolDelete(req); } else { // Free the request data asynchronously req->task.func = finishRequest; @@ -591,7 +592,7 @@ void particle::UsbControlRequestChannel::finishRequest(Request* req) { if (req->handler) { req->handler(req->result, req->handlerData); } - system_pool_free(req, nullptr); + systemPoolDelete(req); } // Note: This method is called from an ISR @@ -619,7 +620,7 @@ void particle::UsbControlRequestChannel::allocRequestData(ISRTaskQueue::Task* is } if (req) { // Request has been cancelled t_free(req->request_data); - system_pool_free(req, nullptr); + systemPoolDelete(req); } } diff --git a/system/src/util/system_timer.cpp b/system/src/util/system_timer.cpp new file mode 100644 index 0000000000..7e2cc242fa --- /dev/null +++ b/system/src/util/system_timer.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2024 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#if !defined(DEBUG_BUILD) && !defined(UNIT_TEST) +#define NDEBUG // TODO: Define NDEBUG in release builds +#endif + +#include + +#include "system_timer.h" + +#include "system_error.h" +#include "logging.h" +#include "debug.h" + +namespace particle::system { + +SystemTimer::~SystemTimer() { + stop(); + if (timer_) { + os_timer_destroy(timer_, nullptr); + } +} + +int SystemTimer::start(unsigned timeout) { + stop(); + // Period of an RTOS timer must be greater than 0 + if (timeout > 0) { + if (!timer_) { + int r = os_timer_create(&timer_, timeout, timerCallback, this /* timer_id */, true /* one_shot */, nullptr /* reserved */); + if (r != 0) { + return SYSTEM_ERROR_NO_MEMORY; + } + } else { + int r = os_timer_change(timer_, OS_TIMER_CHANGE_PERIOD, false /* fromISR */, timeout, 0xffffffffu /* block */, nullptr /* reserved */); + if (r != 0) { + return SYSTEM_ERROR_INTERNAL; // Should not happen + } + } + int r = os_timer_change(timer_, OS_TIMER_CHANGE_START, false /* fromISR */, 0 /* period */, 0xffffffffu /* block */, nullptr /* reserved */); + if (r != 0) { + return SYSTEM_ERROR_INTERNAL; // Should not happen + } + } else { + // Schedule a call in the system thread + SystemISRTaskQueue.enqueue(this); + } + return 0; +} + +void SystemTimer::stop() { + if (timer_) { + int r = os_timer_change(timer_, OS_TIMER_CHANGE_STOP, false /* fromISR */, 0 /* period */, 0xffffffffu /* block */, nullptr /* reserved */); + if (r != 0) { + LOG_DEBUG(ERROR, "Failed to stop timer"); // Should not happen + } + } + SystemISRTaskQueue.remove(this); +} + +void SystemTimer::taskCallback(ISRTaskQueue::Task* task) { + auto self = static_cast(task); + assert(self && self->callback_); + self->callback_(self->arg_); +} + +void SystemTimer::timerCallback(os_timer_t timer) { + void* id = nullptr; + int r = os_timer_get_id(timer, &id); + if (r != 0) { + LOG_DEBUG(ERROR, "Failed to get timer ID"); // Should not happen + return; + } + auto self = static_cast(id); + assert(self); + SystemISRTaskQueue.enqueue(self); +} + +} // namespace particle::system diff --git a/system/src/util/system_timer.h b/system/src/util/system_timer.h new file mode 100644 index 0000000000..45e065c964 --- /dev/null +++ b/system/src/util/system_timer.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include "system_threading.h" + +#include "concurrent_hal.h" + +namespace particle::system { + +/** + * A one-shot timer executed in the system thread. + */ +class SystemTimer: private ISRTaskQueue::Task { +public: + typedef void (*Callback)(void* arg); + + /** + * Construct a timer. + * + * @param callback Callback to invoke when the timer expires. + * @param arg Argument to pass to the callback. + */ + explicit SystemTimer(Callback callback, void* arg = nullptr) : + Task(taskCallback), + timer_(), + callback_(callback), + arg_(arg) { + } + + /** + * Destruct the timer. + */ + ~SystemTimer(); + + /** + * Start the timer. + * + * @param timeout Timeout in milliseconds. + * @return 0 on success, otherwise an error code defined by the `system_error_t` enum. + */ + int start(unsigned timeout); + + /** + * Stop the timer. + */ + void stop(); + +private: + os_timer_t timer_; + Callback callback_; + void* arg_; + + static void taskCallback(ISRTaskQueue::Task* task); + static void timerCallback(os_timer_t timer); +}; + +} // namespace particle::system diff --git a/test/unit_tests/cellular/CMakeLists.txt b/test/unit_tests/cellular/CMakeLists.txt index f91495b77e..26af446db2 100644 --- a/test/unit_tests/cellular/CMakeLists.txt +++ b/test/unit_tests/cellular/CMakeLists.txt @@ -14,8 +14,13 @@ include_directories( add_executable( ${target_name} ${DEVICE_OS_DIR}/wiring/src/spark_wiring_cellular_printable.cpp ${DEVICE_OS_DIR}/wiring/src/spark_wiring_print.cpp + ${DEVICE_OS_DIR}/wiring/src/spark_wiring_variant.cpp + ${DEVICE_OS_DIR}/wiring/src/spark_wiring_json.cpp + ${DEVICE_OS_DIR}/wiring/src/spark_wiring_string.cpp + ${DEVICE_OS_DIR}/wiring/src/string_convert.cpp ${DEVICE_OS_DIR}/hal/network/ncp/cellular/network_config_db.cpp ${DEVICE_OS_DIR}/hal/shared/cellular_sig_perc_mapping.cpp + ${DEVICE_OS_DIR}/services/src/jsmn.c cellular.cpp ) diff --git a/test/unit_tests/communication/CMakeLists.txt b/test/unit_tests/communication/CMakeLists.txt index 58d8f95628..c07130d44b 100644 --- a/test/unit_tests/communication/CMakeLists.txt +++ b/test/unit_tests/communication/CMakeLists.txt @@ -17,6 +17,8 @@ add_executable( ${target_name} ${DEVICE_OS_DIR}/communication/src/firmware_update.cpp ${DEVICE_OS_DIR}/communication/src/description.cpp ${DEVICE_OS_DIR}/communication/src/protocol_util.cpp + ${DEVICE_OS_DIR}/communication/src/protocol_defs.cpp + ${DEVICE_OS_DIR}/communication/src/coap_channel_new.cpp ${DEVICE_OS_DIR}/services/src/system_error.cpp ${DEVICE_OS_DIR}/services/src/jsmn.c ${DEVICE_OS_DIR}/wiring/src/spark_wiring_json.cpp diff --git a/test/unit_tests/communication/hal_stubs.cpp b/test/unit_tests/communication/hal_stubs.cpp index ad6dec73a8..dd27920263 100644 --- a/test/unit_tests/communication/hal_stubs.cpp +++ b/test/unit_tests/communication/hal_stubs.cpp @@ -10,6 +10,12 @@ #include "logging.h" #include "diagnostics.h" +namespace particle::protocol { + +class Protocol; + +} // namespace particle::protocol + extern "C" uint32_t HAL_RNG_GetRandomNumber() { return rand(); @@ -44,3 +50,7 @@ extern "C" void log_write(int level, const char *category, const char *data, siz extern "C" int diag_register_source(const diag_source* src, void* reserved) { return 0; } + +extern "C" particle::protocol::Protocol* spark_protocol_instance(void) { + return nullptr; +} diff --git a/test/unit_tests/stub/filesystem.h b/test/unit_tests/stub/filesystem.h index cb9876ab7e..703673ab5f 100644 --- a/test/unit_tests/stub/filesystem.h +++ b/test/unit_tests/stub/filesystem.h @@ -118,7 +118,7 @@ class FsLock { } void lock() { - filesystem_unlock(fs_); + filesystem_lock(fs_); } void unlock() { diff --git a/test/unit_tests/util/stream.h b/test/unit_tests/util/stream.h index 4e56227189..6b43b03a7c 100644 --- a/test/unit_tests/util/stream.h +++ b/test/unit_tests/util/stream.h @@ -1,11 +1,13 @@ #ifndef TEST_TOOLS_STREAM_H #define TEST_TOOLS_STREAM_H -#include "spark_wiring_print.h" +#include "spark_wiring_stream.h" #include "check.h" +#include #include +#include namespace test { @@ -27,6 +29,61 @@ class OutputStream: std::string s_; }; +class Stream: public ::Stream { +public: + explicit Stream(std::string data = std::string()) : + data_(std::move(data)), + readPos_(0) { + } + + size_t readBytes(char* data, size_t size) override { + size_t n = std::min(size, data_.size() - readPos_); + std::memcpy(data, data_.data() + readPos_, n); + readPos_ += n; + return n; + } + + int read() override { + uint8_t b; + size_t n = readBytes((char*)&b, 1); + if (n != 1) { + return -1; + } + return b; + } + + int peek() override { + if (data_.size() - readPos_ == 0) { + return -1; + } + return (uint8_t)data_.at(readPos_); + } + + int available() override { + return data_.size() - readPos_; + } + + size_t write(const uint8_t* data, size_t size) override { + data_.append((const char*)data, size); + return size; + } + + size_t write(uint8_t b) override { + return write(&b, 1); + } + + void flush() override { + } + + const std::string& data() const { + return data_; + } + +private: + std::string data_; + size_t readPos_; +}; + } // namespace test // test::OutputStream diff --git a/test/unit_tests/wiring/CMakeLists.txt b/test/unit_tests/wiring/CMakeLists.txt index 05f85b41fb..d439a865cf 100644 --- a/test/unit_tests/wiring/CMakeLists.txt +++ b/test/unit_tests/wiring/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable( ${target_name} ${DEVICE_OS_DIR}/services/src/jsmn.c ${DEVICE_OS_DIR}/wiring/src/spark_wiring_async.cpp ${DEVICE_OS_DIR}/wiring/src/spark_wiring_print.cpp + ${DEVICE_OS_DIR}/wiring/src/spark_wiring_stream.cpp ${DEVICE_OS_DIR}/wiring/src/spark_wiring_random.cpp ${DEVICE_OS_DIR}/wiring/src/spark_wiring_string.cpp ${DEVICE_OS_DIR}/wiring/src/spark_wiring_wifi.cpp @@ -17,11 +18,13 @@ add_executable( ${target_name} ${DEVICE_OS_DIR}/wiring/src/spark_wiring_fuel.cpp ${DEVICE_OS_DIR}/wiring/src/spark_wiring_i2c.cpp ${DEVICE_OS_DIR}/wiring/src/spark_wiring_ipaddress.cpp + ${DEVICE_OS_DIR}/wiring/src/spark_wiring_variant.cpp ${DEVICE_OS_DIR}/wiring_globals/src/wiring_globals_i2c.cpp ${DEVICE_OS_DIR}/hal/src/template/i2c_hal.cpp ${DEVICE_OS_DIR}/wiring/src/string_convert.cpp ${TEST_DIR}/util/alloc.cpp ${TEST_DIR}/util/buffer.cpp + ${TEST_DIR}/util/string.cpp ${TEST_DIR}/util/random_old.cpp ${TEST_DIR}/stub/system_network.cpp ${TEST_DIR}/stub/inet_hal_compat.cpp @@ -39,6 +42,8 @@ add_executable( ${target_name} json.cpp std_functions.cpp wlan.cpp + map.cpp + variant.cpp ) # Set defines specific to target diff --git a/test/unit_tests/wiring/json.cpp b/test/unit_tests/wiring/json.cpp index 127822d54b..2330f4d3af 100644 --- a/test/unit_tests/wiring/json.cpp +++ b/test/unit_tests/wiring/json.cpp @@ -595,7 +595,18 @@ TEST_CASE("Writing JSON") { json.value(DBL_MAX); check(data).equals("1.79769e+308"); // Based on DBL_DIG significant digits for '%g' format specifier } - + SECTION("NaN") { + json.value(NAN); + check(data).equals("0"); + } + SECTION("positive infinity") { + json.value(INFINITY); + check(data).equals("1.79769e+308"); // DBL_MAX formatted with default precision + } + SECTION("negative infinity") { + json.value(-INFINITY); + check(data).equals("-1.79769e+308"); // -DBL_MAX formatted with default precision + } // Precision related tests @@ -987,6 +998,8 @@ TEST_CASE("Writing JSON") { check(data).equals("{\"a\\tb\\n\":\"a\\tb\\n\"}"); } } + + CHECK(json.bytesWritten() == data.size()); } TEST_CASE("JSONValue") { diff --git a/test/unit_tests/wiring/map.cpp b/test/unit_tests/wiring/map.cpp new file mode 100644 index 0000000000..f4ccf96063 --- /dev/null +++ b/test/unit_tests/wiring/map.cpp @@ -0,0 +1,75 @@ +#include +#include + +#include "spark_wiring_map.h" + +#include "util/catch.h" + +using namespace particle; + +namespace { + +template +void checkMap(const MapT& map, const std::vector& expectedEntries) { + auto& entries = map.entries(); + REQUIRE(entries.size() == expectedEntries.size()); + for (int i = 0; i < entries.size(); ++i) { + CHECK(entries[i] == expectedEntries[i]); + CHECK(map.has(entries[i].first)); + } + CHECK(map.size() == entries.size()); + CHECK(map.capacity() >= map.size()); + CHECK(((map.isEmpty() && map.size() == 0) || (!map.isEmpty() && map.size() > 0))); +} + +} // namespace + +TEST_CASE("Map") { + SECTION("Map()") { + Map m; + checkMap(m, {}); + } + + SECTION("set()") { + Map m; + m.set("b", 2); + checkMap(m, { { "b", 2 } }); + m.set("c", 3); + checkMap(m, { { "b", 2 }, { "c", 3 } }); + m.set("a", 1); + checkMap(m, { { "a", 1 }, { "b", 2 }, { "c", 3 } }); + } + + SECTION("get()") { + Map m({ { "a", 1 }, { "b", 2 }, { "c", 3 } }); + CHECK(m.get("a") == 1); + CHECK(m.get("b") == 2); + CHECK(m.get("c") == 3); + CHECK(m.get("d", 4) == 4); + } + + SECTION("remove()") { + Map m({ { "a", 1 }, { "b", 2 }, { "c", 3 } }); + CHECK(m.remove("b")); + checkMap(m, { { "a", 1 }, { "c", 3 } }); + CHECK(m.remove("c")); + checkMap(m, { { "a", 1 } }); + CHECK(!m.remove("d")); + checkMap(m, { { "a", 1 } }); + CHECK(m.remove("a")); + checkMap(m, {}); + } + + SECTION("operator[]") { + Map m; + m["b"] = 2; + checkMap(m, { { "b", 2 } }); + m["c"] = 3; + checkMap(m, { { "b", 2 }, { "c", 3 } }); + m["a"] = 1; + checkMap(m, { { "a", 1 }, { "b", 2 }, { "c", 3 } }); + CHECK(m["a"] == 1); + CHECK(m["b"] == 2); + CHECK(m["c"] == 3); + } +} diff --git a/test/unit_tests/wiring/variant.cpp b/test/unit_tests/wiring/variant.cpp new file mode 100644 index 0000000000..05ac4fc264 --- /dev/null +++ b/test/unit_tests/wiring/variant.cpp @@ -0,0 +1,474 @@ +#include +#include + +#include "spark_wiring_variant.h" + +#include "util/stream.h" +#include "util/string.h" +#include "util/catch.h" + +using namespace particle; + +namespace { + +template +void checkVariant(const Variant& v, const T& expectedValue = T()) { + REQUIRE(v.is()); + CHECK(v.value() == expectedValue); + CHECK(v.to() == expectedValue); + bool ok = false; + CHECK(v.to(ok) == expectedValue); + CHECK(ok); + CHECK(v == expectedValue); + CHECK(expectedValue == v); + CHECK_FALSE(v != expectedValue); + CHECK_FALSE(expectedValue != v); + if constexpr (std::is_same_v) { + CHECK(v.type() == Variant::NULL_); + CHECK((v.isNull() && !v.isBool() && !v.isInt() && !v.isUInt() && !v.isInt64() && !v.isUInt64() && !v.isDouble() && !v.isNumber() && !v.isString() && !v.isArray() && !v.isMap())); + } else if constexpr (std::is_same_v) { + CHECK(v.type() == Variant::BOOL); + CHECK((!v.isNull() && v.isBool() && !v.isInt() && !v.isUInt() && !v.isInt64() && !v.isUInt64() && !v.isDouble() && !v.isNumber() && !v.isString() && !v.isArray() && !v.isMap())); + CHECK(v.toBool() == expectedValue); + bool ok = false; + CHECK(v.toBool(ok) == expectedValue); + CHECK(ok); + } else if constexpr (std::is_same_v) { + CHECK(v.type() == Variant::INT); + CHECK((!v.isNull() && !v.isBool() && v.isInt() && !v.isUInt() && !v.isInt64() && !v.isUInt64() && !v.isDouble() && v.isNumber() && !v.isString() && !v.isArray() && !v.isMap())); + CHECK(v.toInt() == expectedValue); + bool ok = false; + CHECK(v.toInt(ok) == expectedValue); + CHECK(ok); + } else if constexpr (std::is_same_v) { + CHECK(v.type() == Variant::UINT); + CHECK((!v.isNull() && !v.isBool() && !v.isInt() && v.isUInt() && !v.isInt64() && !v.isUInt64() && !v.isDouble() && v.isNumber() && !v.isString() && !v.isArray() && !v.isMap())); + CHECK(v.toUInt() == expectedValue); + bool ok = false; + CHECK(v.toUInt(ok) == expectedValue); + CHECK(ok); + } else if constexpr (std::is_same_v) { + CHECK(v.type() == Variant::INT64); + CHECK((!v.isNull() && !v.isBool() && !v.isInt() && !v.isUInt() && v.isInt64() && !v.isUInt64() && !v.isDouble() && v.isNumber() && !v.isString() && !v.isArray() && !v.isMap())); + CHECK(v.toInt64() == expectedValue); + bool ok = false; + CHECK(v.toInt64(ok) == expectedValue); + CHECK(ok); + } else if constexpr (std::is_same_v) { + CHECK(v.type() == Variant::UINT64); + CHECK((!v.isNull() && !v.isBool() && !v.isInt() && !v.isUInt() && !v.isInt64() && v.isUInt64() && !v.isDouble() && v.isNumber() && !v.isString() && !v.isArray() && !v.isMap())); + CHECK(v.toUInt64() == expectedValue); + bool ok = false; + CHECK(v.toUInt64(ok) == expectedValue); + CHECK(ok); + } else if constexpr (std::is_same_v) { + CHECK(v.type() == Variant::DOUBLE); + CHECK((!v.isNull() && !v.isBool() && !v.isInt() && !v.isUInt() && !v.isInt64() && !v.isUInt64() && v.isDouble() && v.isNumber() && !v.isString() && !v.isArray() && !v.isMap())); + CHECK(v.toDouble() == expectedValue); + bool ok = false; + CHECK(v.toDouble(ok) == expectedValue); + CHECK(ok); + } else if constexpr (std::is_same_v) { + CHECK(v.type() == Variant::STRING); + CHECK((!v.isNull() && !v.isBool() && !v.isInt() && !v.isUInt() && !v.isInt64() && !v.isUInt64() && !v.isDouble() && !v.isNumber() && v.isString() && !v.isArray() && !v.isMap())); + CHECK(v.toString() == expectedValue); + bool ok = false; + CHECK(v.toString(ok) == expectedValue); + CHECK(ok); + } else if constexpr (std::is_same_v) { + CHECK(v.type() == Variant::ARRAY); + CHECK((!v.isNull() && !v.isBool() && !v.isInt() && !v.isUInt() && !v.isInt64() && !v.isUInt64() && !v.isDouble() && !v.isNumber() && !v.isString() && v.isArray() && !v.isMap())); + CHECK(v.toArray() == expectedValue); + bool ok = false; + CHECK(v.toArray(ok) == expectedValue); + CHECK(ok); + } else if constexpr (std::is_same_v) { + CHECK(v.type() == Variant::MAP); + CHECK((!v.isNull() && !v.isBool() && !v.isInt() && !v.isUInt() && !v.isInt64() && !v.isUInt64() && !v.isDouble() && !v.isNumber() && !v.isString() && !v.isArray() && v.isMap())); + CHECK(v.toMap() == expectedValue); + bool ok = false; + CHECK(v.toMap(ok) == expectedValue); + CHECK(ok); + } else { + FAIL("Unexpected type"); + } +} + +template +void checkVariant(Variant& v, const T& expectedValue = T()) { + // Test const methods + checkVariant(static_cast(v), expectedValue); + // Test non-const methods + CHECK(v.as() == expectedValue); + if constexpr (std::is_same_v) { + CHECK(v.asBool() == expectedValue); + } else if constexpr (std::is_same_v) { + CHECK(v.asInt() == expectedValue); + } else if constexpr (std::is_same_v) { + CHECK(v.asUInt() == expectedValue); + } else if constexpr (std::is_same_v) { + CHECK(v.asInt64() == expectedValue); + } else if constexpr (std::is_same_v) { + CHECK(v.asUInt64() == expectedValue); + } else if constexpr (std::is_same_v) { + CHECK(v.asDouble() == expectedValue); + } else if constexpr (std::is_same_v) { + CHECK(v.asString() == expectedValue); + } else if constexpr (std::is_same_v) { + CHECK(v.asArray() == expectedValue); + } else if constexpr (std::is_same_v) { + CHECK(v.asMap() == expectedValue); + } else if constexpr (!std::is_same_v) { + FAIL("Unexpected type"); + } +} + +std::string toCbor(const Variant& v) { + test::Stream s; + REQUIRE(encodeToCBOR(v, s) == 0); + return s.data(); +} + +Variant fromCbor(const std::string& data) { + test::Stream s(data); + Variant v; + REQUIRE(decodeFromCBOR(v, s) == 0); + return v; +} + +} // namespace + +TEST_CASE("Variant") { + SECTION("can be constructed from a value of one of the supported types") { + { + Variant v; + checkVariant(v); + } + { + Variant v(std::monostate{}); + checkVariant(v); + } + { + Variant v(true); + checkVariant(v, true); + } + { + Variant v((char)123); + checkVariant(v, 123); + } + { + Variant v((unsigned char)123); + checkVariant(v, 123); + } + { + Variant v((short)123); + checkVariant(v, 123); + } + { + Variant v((unsigned short)123); + checkVariant(v, 123); + } + { + Variant v(123); + checkVariant(v, 123); + } + { + Variant v(123u); + checkVariant(v, 123); + } +#ifdef __LP64__ + { + Variant v(123l); + checkVariant(v, 123); + } + { + Variant v(123ul); + checkVariant(v, 123); + } +#else + { + Variant v(123l); + checkVariant(v, 123); + } + { + Variant v(123ul); + checkVariant(v, 123); + } +#endif + { + Variant v(123ll); + checkVariant(v, 123); + } + { + Variant v(123ull); + checkVariant(v, 123); + } + { + Variant v(123.0f); + checkVariant(v, 123.0); + } + { + Variant v(123.0); + checkVariant(v, 123.0); + } + { + Variant v("abc"); + checkVariant(v, "abc"); + } + { + Variant v(String("abc")); + checkVariant(v, "abc"); + } + { + VariantArray arr({ 1, 2, 3 }); + Variant v(arr); + checkVariant(v, arr); + } + { + VariantMap map({ { "a", 1 }, { "b", 2 }, { "c", 3 } }); + Variant v(map); + checkVariant(v, map); + } + } + + SECTION("can be assigned a value of one of the supported types") { + Variant v; + + v = std::monostate{}; + checkVariant(v); + + v = true; + checkVariant(v, true); + + v = (char)123; + checkVariant(v, 123); + + v = (unsigned char)123; + checkVariant(v, 123); + + v = (short)123; + checkVariant(v, 123); + + v = (unsigned short)123; + checkVariant(v, 123); + + v = 123; + checkVariant(v, 123); + + v = 123u; + checkVariant(v, 123); +#ifdef __LP64__ + v = 123l; + checkVariant(v, 123); + + v = 123ul; + checkVariant(v, 123); +#else + v = 123l; + checkVariant(v, 123); + + v = 123ul; + checkVariant(v, 123); +#endif + v = 123ll; + checkVariant(v, 123); + + v = 123ull; + checkVariant(v, 123); + + v = 123.0f; + checkVariant(v, 123.0); + + v = 123.0; + checkVariant(v, 123.0); + + v = "abc"; + checkVariant(v, "abc"); + + v = String("abc"); + checkVariant(v, "abc"); + + VariantArray arr({ 1, 2, 3 }); + v = arr; + checkVariant(v, arr); + + VariantMap map({ { "a", 1 }, { "b", 2 }, { "c", 3 } }); + v = map; + checkVariant(v, map); + } + + SECTION("can be converted to JSON") { + Variant v; + CHECK(v.toJSON() == "null"); + + v = true; + CHECK(v.toJSON() == "true"); + + v = 123; + CHECK(v.toJSON() == "123"); + + v = 123.5; + CHECK(v.toJSON() == "123.5"); + + v = "abc"; + CHECK(v.toJSON() == "\"abc\""); + + v.append(123); + v.append("abc"); + CHECK(v.toJSON() == "[123,\"abc\"]"); + + v.set("a", 1); + v.set("b", 2); + v.set("c", 3); + CHECK(v.toJSON() == "{\"a\":1,\"b\":2,\"c\":3}"); + } + + SECTION("can be converted from JSON") { + Variant v = Variant::fromJSON("null"); + checkVariant(v); + + v = Variant::fromJSON("true"); + checkVariant(v, true); + + v = Variant::fromJSON("123"); + checkVariant(v, 123); + + v = Variant::fromJSON("123.5"); + checkVariant(v, 123.5); + + v = Variant::fromJSON("\"abc\""); + checkVariant(v, String("abc")); + + v = Variant::fromJSON("[123,\"abc\"]"); + checkVariant(v, VariantArray{ 123, "abc" }); + + v = Variant::fromJSON("{\"a\":1,\"b\":2,\"c\":3}"); + checkVariant(v, VariantMap{ { "a", 1 }, { "b", 2 }, { "c", 3 } }); + } + + SECTION("encodeVariantToCBOR()") { + using test::toHex; + CHECK(toHex(toCbor(0)) == "00"); + CHECK(toHex(toCbor(1)) == "01"); + CHECK(toHex(toCbor(10)) == "0a"); + CHECK(toHex(toCbor(23)) == "17"); + CHECK(toHex(toCbor(24)) == "1818"); + CHECK(toHex(toCbor(25)) == "1819"); + CHECK(toHex(toCbor(100)) == "1864"); + CHECK(toHex(toCbor(1000)) == "1903e8"); + CHECK(toHex(toCbor(1000000)) == "1a000f4240"); + CHECK(toHex(toCbor(1000000000000ull)) == "1b000000e8d4a51000"); + CHECK(toHex(toCbor(18446744073709551615ull)) == "1bffffffffffffffff"); + CHECK(toHex(toCbor(-9223372036854775807ll - 1)) == "3b7fffffffffffffff"); + CHECK(toHex(toCbor(-1)) == "20"); + CHECK(toHex(toCbor(-10)) == "29"); + CHECK(toHex(toCbor(-100)) == "3863"); + CHECK(toHex(toCbor(-1000)) == "3903e7"); + CHECK(toHex(toCbor(0.0)) == "fa00000000"); // Encoding half-precision floats is not supported + CHECK(toHex(toCbor(-0.0)) == "fa80000000"); // ditto + CHECK(toHex(toCbor(1.0)) == "fa3f800000"); // ditto + CHECK(toHex(toCbor(1.1)) == "fb3ff199999999999a"); + CHECK(toHex(toCbor(1.5)) == "fa3fc00000"); // ditto + CHECK(toHex(toCbor(100000.0)) == "fa47c35000"); + CHECK(toHex(toCbor(16777216.0)) == "fa4b800000"); + CHECK(toHex(toCbor(3.4028234663852886e+38)) == "fa7f7fffff"); + CHECK(toHex(toCbor(1.0e+300)) == "fb7e37e43c8800759c"); + CHECK(toHex(toCbor(1.401298464324817e-45)) == "fa00000001"); + CHECK(toHex(toCbor(1.1754943508222875e-38)) == "fa00800000"); + CHECK(toHex(toCbor(-4.0)) == "fac0800000"); // ditto + CHECK(toHex(toCbor(-4.1)) == "fbc010666666666666"); + CHECK(toHex(toCbor(INFINITY)) == "fa7f800000"); // ditto + CHECK(toHex(toCbor(-INFINITY)) == "faff800000"); // ditto + CHECK(toHex(toCbor(NAN)) == "fb7ff8000000000000"); // For simplicity, NaN is always encoded as a double + CHECK(toHex(toCbor(false)) == "f4"); + CHECK(toHex(toCbor(true)) == "f5"); + CHECK(toHex(toCbor(Variant())) == "f6"); + CHECK(toHex(toCbor("")) == "60"); + CHECK(toHex(toCbor("a")) == "6161"); + CHECK(toHex(toCbor("IETF")) == "6449455446"); + CHECK(toHex(toCbor("\"\\")) == "62225c"); + CHECK(toHex(toCbor("\u00fc")) == "62c3bc"); + CHECK(toHex(toCbor("\u6c34")) == "63e6b0b4"); + CHECK(toHex(toCbor(VariantArray{})) == "80"); + CHECK(toHex(toCbor(VariantArray{1, 2, 3})) == "83010203"); + CHECK(toHex(toCbor(VariantArray{1, VariantArray{2, 3}, VariantArray{4, 5}})) == "8301820203820405"); + CHECK(toHex(toCbor(VariantArray{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})) == "98190102030405060708090a0b0c0d0e0f101112131415161718181819"); + CHECK(toHex(toCbor(VariantMap{})) == "a0"); + CHECK(toHex(toCbor(VariantMap{{"a", 1}, {"b", VariantArray{2, 3}}})) == "a26161016162820203"); + CHECK(toHex(toCbor(VariantArray{"a", VariantMap{{"b", "c"}}})) == "826161a161626163"); + CHECK(toHex(toCbor(VariantMap{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}, {"e", "E"}})) == "a56161614161626142616361436164614461656145"); + } + + SECTION("decodeVariantFromCBOR") { + using test::fromHex; + CHECK(fromCbor(fromHex("00")) == Variant(0)); + CHECK(fromCbor(fromHex("01")) == Variant(1)); + CHECK(fromCbor(fromHex("0a")) == Variant(10)); + CHECK(fromCbor(fromHex("17")) == Variant(23)); + CHECK(fromCbor(fromHex("1818")) == Variant(24)); + CHECK(fromCbor(fromHex("1819")) == Variant(25)); + CHECK(fromCbor(fromHex("1864")) == Variant(100)); + CHECK(fromCbor(fromHex("1903e8")) == Variant(1000)); + CHECK(fromCbor(fromHex("1a000f4240")) == Variant(1000000)); + CHECK(fromCbor(fromHex("1b000000e8d4a51000")) == Variant(1000000000000ull)); + CHECK(fromCbor(fromHex("1bffffffffffffffff")) == Variant(18446744073709551615ull)); + CHECK(fromCbor(fromHex("3b7fffffffffffffff")) == Variant(-9223372036854775807ll - 1)); + CHECK(fromCbor(fromHex("20")) == Variant(-1)); + CHECK(fromCbor(fromHex("29")) == Variant(-10)); + CHECK(fromCbor(fromHex("3863")) == Variant(-100)); + CHECK(fromCbor(fromHex("3903e7")) == Variant(-1000)); + CHECK(fromCbor(fromHex("f90000")) == Variant(0.0)); + CHECK(fromCbor(fromHex("f98000")) == Variant(-0.0)); + CHECK(fromCbor(fromHex("f93c00")) == Variant(1.0)); + CHECK(fromCbor(fromHex("fb3ff199999999999a")) == Variant(1.1)); + CHECK(fromCbor(fromHex("f93e00")) == Variant(1.5)); + CHECK(fromCbor(fromHex("f97bff")) == Variant(65504.0)); + CHECK(fromCbor(fromHex("fa47c35000")) == Variant(100000.0)); + CHECK(fromCbor(fromHex("fa7f7fffff")) == Variant(3.4028234663852886e+38)); + CHECK(fromCbor(fromHex("fb7e37e43c8800759c")) == Variant(1.0e+300)); + CHECK(fromCbor(fromHex("f90001")) == Variant(5.960464477539063e-8)); + CHECK(fromCbor(fromHex("f90400")) == Variant(0.00006103515625)); + CHECK(fromCbor(fromHex("f9c400")) == Variant(-4.0)); + CHECK(fromCbor(fromHex("fbc010666666666666")) == Variant(-4.1)); + CHECK(fromCbor(fromHex("f97c00")) == Variant(INFINITY)); + CHECK(std::isnan(fromCbor(fromHex("f97e00")).asDouble())); + CHECK(fromCbor(fromHex("f9fc00")) == Variant(-INFINITY)); + CHECK(fromCbor(fromHex("fa7f800000")) == Variant(INFINITY)); + CHECK(std::isnan(fromCbor(fromHex("fa7fc00000")).asDouble())); + CHECK(fromCbor(fromHex("faff800000")) == Variant(-INFINITY)); + CHECK(fromCbor(fromHex("fb7ff0000000000000")) == Variant(INFINITY)); + CHECK(std::isnan(fromCbor(fromHex("fb7ff8000000000000")).asDouble())); + CHECK(fromCbor(fromHex("fbfff0000000000000")) == Variant(-INFINITY)); + CHECK(fromCbor(fromHex("f4")) == Variant(false)); + CHECK(fromCbor(fromHex("f5")) == Variant(true)); + CHECK(fromCbor(fromHex("f6")) == Variant()); + CHECK(fromCbor(fromHex("c074323031332d30332d32315432303a30343a30305a")) == Variant("2013-03-21T20:04:00Z")); + CHECK(fromCbor(fromHex("c11a514b67b0")) == Variant(1363896240)); + CHECK(fromCbor(fromHex("c1fb41d452d9ec200000")) == Variant(1363896240.5)); + CHECK(fromCbor(fromHex("d82076687474703a2f2f7777772e6578616d706c652e636f6d")) == Variant("http://www.example.com")); + CHECK(fromCbor(fromHex("60")) == Variant("")); + CHECK(fromCbor(fromHex("6161")) == Variant("a")); + CHECK(fromCbor(fromHex("6449455446")) == Variant("IETF")); + CHECK(fromCbor(fromHex("62225c")) == Variant("\"\\")); + CHECK(fromCbor(fromHex("62c3bc")) == Variant("\u00fc")); + CHECK(fromCbor(fromHex("63e6b0b4")) == Variant("\u6c34")); + CHECK(fromCbor(fromHex("80")) == VariantArray{}); + CHECK(fromCbor(fromHex("83010203")) == VariantArray{1, 2, 3}); + CHECK(fromCbor(fromHex("8301820203820405")) == VariantArray{1, VariantArray{2, 3}, VariantArray{4, 5}}); + CHECK(fromCbor(fromHex("98190102030405060708090a0b0c0d0e0f101112131415161718181819")) == VariantArray{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}); + CHECK(fromCbor(fromHex("a0")) == VariantMap{}); + CHECK(fromCbor(fromHex("a26161016162820203")) == VariantMap{{"a", 1}, {"b", VariantArray{2, 3}}}); + CHECK(fromCbor(fromHex("826161a161626163")) == VariantArray{"a", VariantMap{{"b", "c"}}}); + CHECK(fromCbor(fromHex("a56161614161626142616361436164614461656145")) == VariantMap{{"a", "A"}, {"b", "B"}, {"c", "C"}, {"d", "D"}, {"e", "E"}}); + CHECK(fromCbor(fromHex("7f657374726561646d696e67ff")) == Variant("streaming")); + CHECK(fromCbor(fromHex("9fff")) == VariantArray{}); + CHECK(fromCbor(fromHex("9f018202039f0405ffff")) == VariantArray{1, VariantArray{2, 3}, VariantArray{4, 5}}); + CHECK(fromCbor(fromHex("9f01820203820405ff")) == VariantArray{1, VariantArray{2, 3}, VariantArray{4, 5}}); + CHECK(fromCbor(fromHex("83018202039f0405ff")) == VariantArray{1, VariantArray{2, 3}, VariantArray{4, 5}}); + CHECK(fromCbor(fromHex("83019f0203ff820405")) == VariantArray{1, VariantArray{2, 3}, VariantArray{4, 5}}); + CHECK(fromCbor(fromHex("9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff")) == VariantArray{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}); + CHECK(fromCbor(fromHex("bf61610161629f0203ffff")) == VariantMap{{"a", 1}, {"b", VariantArray{2, 3}}}); + CHECK(fromCbor(fromHex("826161bf61626163ff")) == VariantArray{"a", VariantMap{{"b", "c"}}}); + CHECK(fromCbor(fromHex("bf6346756ef563416d7421ff")) == VariantMap{{"Fun", true}, {"Amt", -2}}); + } +} diff --git a/test/unit_tests/wiring/vector.cpp b/test/unit_tests/wiring/vector.cpp index 70f057697c..1a5362aa70 100644 --- a/test/unit_tests/wiring/vector.cpp +++ b/test/unit_tests/wiring/vector.cpp @@ -431,6 +431,26 @@ void testVector() { check(a).values(1, 2, 3).capacity(3); } } + SECTION("insert(ConstIterator pos, T value)") { + Vector a({ 2, 4, 5 }); + auto it = a.insert(a.begin(), 1); // insert at the beginning + CHECK(it == a.begin()); + CHECK(*it == 1); + check(a).values(1, 2, 4, 5).capacity(4); + it = a.insert(a.end(), 6); // insert at the end + CHECK(it == a.end() - 1); + CHECK(*it == 6); + check(a).values(1, 2, 4, 5, 6).capacity(5); + it = a.insert(a.begin() + 2, 3); // insert in the middle + CHECK(it == a.begin() + 2); + CHECK(*it == 3); + check(a).values(1, 2, 3, 4, 5, 6).capacity(6); + Vector b; + it = b.insert(b.begin(), 1); // insert to empty vector + CHECK(it == b.begin()); + CHECK(*it == 1); + check(b).values(1).capacity(1); + } } SECTION("removeAt(int i, int n)") { @@ -498,6 +518,19 @@ void testVector() { REQUIRE(a.removeAll(1) == 0); // remove from empty array } + SECTION("erase(ConstIterator)") { + Vector a({ 1, 2, 3, 4, 5 }); + auto it = a.erase(a.begin()); // erase the first element + CHECK(it == a.begin()); + check(a).values(2, 3, 4, 5).capacity(5); + it = a.erase(a.end() - 1); // erase the last element + CHECK(it == a.end()); + check(a).values(2, 3, 4).capacity(5); + it = a.erase(a.begin() + 1); // erase an element in the middle + CHECK(it == a.end() - 1); + check(a).values(2, 4).capacity(5); + } + SECTION("takeFirst()") { Vector a({ 1, 2, 3 }); REQUIRE(a.takeFirst() == 1); diff --git a/third_party/mbedtls/mbedtls b/third_party/mbedtls/mbedtls index 7da4060120..f9ab9100ab 160000 --- a/third_party/mbedtls/mbedtls +++ b/third_party/mbedtls/mbedtls @@ -1 +1 @@ -Subproject commit 7da40601200bba5264ba6a5035eb410067c49f29 +Subproject commit f9ab9100ab5de9439db75e25c9400bba89c94330 diff --git a/user/inc/application.h b/user/inc/application.h index de6ca9ec44..5f9b1ec0f4 100644 --- a/user/inc/application.h +++ b/user/inc/application.h @@ -73,10 +73,13 @@ #include "spark_wiring_logging.h" #include "spark_wiring_json.h" #include "spark_wiring_vector.h" +#include "spark_wiring_map.h" +#include "spark_wiring_variant.h" #include "spark_wiring_async.h" #include "spark_wiring_error.h" #include "spark_wiring_led.h" #include "spark_wiring_diagnostics.h" +#include "spark_wiring_ledger.h" #include "fast_pin.h" #include "string_convert.h" #include "debug_output_handler.h" diff --git a/user/tests/app/ledger_perf/ledger_perf.cpp b/user/tests/app/ledger_perf/ledger_perf.cpp new file mode 100644 index 0000000000..e6d7f54226 --- /dev/null +++ b/user/tests/app/ledger_perf/ledger_perf.cpp @@ -0,0 +1,358 @@ +#define LOG_CHECKED_ERRORS 1 // Log errors caught by the CHECK() macro + +#include +#include +#include +#include +#include + +#include "application.h" + +#include "random.h" +#include "scope_guard.h" +#include "check.h" + +SYSTEM_MODE(SEMI_AUTOMATIC) +SYSTEM_THREAD(ENABLED) + +namespace { + +const auto LEDGER_NAME = "test"; + +const size_t SMALL_DATA_SIZE = 100; +const size_t MEDIUM_DATA_SIZE = 1000; +const size_t LARGE_DATA_SIZE = 10000; + +static_assert(SMALL_DATA_SIZE <= LARGE_DATA_SIZE && MEDIUM_DATA_SIZE <= LARGE_DATA_SIZE && + LARGE_DATA_SIZE <= LEDGER_MAX_DATA_SIZE); + +const auto REPEAT_COUNT = 100; + +const auto DESCR_COLUMN_WIDTH = 62; +const auto MIN_MAX_AVG_COLUMN_WIDTH = 7; + +const auto MAX_TICKS = std::numeric_limits::max(); + +struct Stats { + system_tick_t open; + system_tick_t close; + system_tick_t readOrWrite; + system_tick_t getInstance; + system_tick_t release; + system_tick_t openReadOrWriteClose; + system_tick_t total; +}; + +const SerialLogHandler logHandler(LOG_LEVEL_ERROR, { + { "app", LOG_LEVEL_ALL } +}); + +char inBuffer[LARGE_DATA_SIZE]; +char outBuffer[LARGE_DATA_SIZE]; + +int readFromLedger(char* data, size_t size, Stats& stats, const std::function& onStreamOpen = nullptr) { + int r = 0; + // Get the ledger + ledger_instance* ledger = nullptr; + auto t1 = millis(); + r = ledger_get_instance(&ledger, LEDGER_NAME, nullptr); + auto t2 = millis(); + CHECK(r); + NAMED_SCOPE_GUARD(releaseLedgerGuard, { + ledger_release(ledger, nullptr); + }); + // Open a stream + ledger_stream* stream = nullptr; + auto t3 = millis(); + r = ledger_open(&stream, ledger, LEDGER_STREAM_MODE_READ, nullptr); + auto t4 = millis(); + CHECK(r); + NAMED_SCOPE_GUARD(closeStreamGuard, { + int r = ledger_close(stream, 0, nullptr); + if (r < 0) { + LOG(ERROR, "ledger_close() failed: %d", r); + } + }); + if (onStreamOpen) { + CHECK(onStreamOpen()); + } + // Read the data + auto t5 = millis(); + r = ledger_read(stream, data, size, nullptr); + auto t6 = millis(); + CHECK(r); + if ((size_t)r != size) { + LOG(ERROR, "Unexpected size of ledger data"); + return Error::BAD_DATA; + } + char c = 0; + r = ledger_read(stream, &c, 1, nullptr); + if (r != SYSTEM_ERROR_END_OF_STREAM) { + LOG(ERROR, "Unexpected size of ledger data"); + return Error::BAD_DATA; + } + // Close the stream + closeStreamGuard.dismiss(); + auto t7 = millis(); + r = ledger_close(stream, 0, nullptr); + auto t8 = millis(); + CHECK(r); + // Release the ledger + releaseLedgerGuard.dismiss(); + auto t9 = millis(); + ledger_release(ledger, nullptr); + auto t10 = millis(); + + stats.getInstance = t2 - t1; + stats.open = t4 - t3; + stats.readOrWrite = t6 - t5; + stats.close = t8 - t7; + stats.release = t10 - t9; + stats.openReadOrWriteClose = stats.open + stats.readOrWrite + stats.close; + stats.total = stats.getInstance + stats.openReadOrWriteClose + stats.release; + return 0; +} + +int writeToLedger(const char* data, size_t size, Stats& stats) { + int r = 0; + // Get the ledger + ledger_instance* ledger = nullptr; + auto t1 = millis(); + r = ledger_get_instance(&ledger, LEDGER_NAME, nullptr); + auto t2 = millis(); + CHECK(r); + NAMED_SCOPE_GUARD(releaseLedgerGuard, { + ledger_release(ledger, nullptr); + }); + // Open a stream + ledger_stream* stream = nullptr; + auto t3 = millis(); + r = ledger_open(&stream, ledger, LEDGER_STREAM_MODE_WRITE, nullptr); + auto t4 = millis(); + CHECK(r); + NAMED_SCOPE_GUARD(closeStreamGuard, { + int r = ledger_close(stream, LEDGER_STREAM_CLOSE_DISCARD, nullptr); + if (r < 0) { + LOG(ERROR, "ledger_close() failed: %d", r); + } + }); + // Write the data + auto t5 = millis(); + r = ledger_write(stream, data, size, nullptr); + auto t6 = millis(); + CHECK(r); + if ((size_t)r != size) { + LOG(ERROR, "Unexpected number of bytes written"); + return Error::IO; + } + // Close the stream + closeStreamGuard.dismiss(); + auto t7 = millis(); + r = ledger_close(stream, 0, nullptr); + auto t8 = millis(); + CHECK(r); + // Release the ledger + releaseLedgerGuard.dismiss(); + auto t9 = millis(); + ledger_release(ledger, nullptr); + auto t10 = millis(); + + stats.getInstance = t2 - t1; + stats.open = t4 - t3; + stats.readOrWrite = t6 - t5; + stats.close = t8 - t7; + stats.release = t10 - t9; + stats.openReadOrWriteClose = stats.open + stats.readOrWrite + stats.close; + stats.total = stats.getInstance + stats.openReadOrWriteClose + stats.release; + return 0; +} + +void updateStats(const Stats& stats, Stats& min, Stats& max, Stats& total) { + min.open = std::min(min.open, stats.open); + min.close = std::min(min.close, stats.close); + min.readOrWrite = std::min(min.readOrWrite, stats.readOrWrite); + min.getInstance = std::min(min.getInstance, stats.getInstance); + min.release = std::min(min.release, stats.release); + min.openReadOrWriteClose = std::min(min.openReadOrWriteClose, stats.openReadOrWriteClose); + min.total = std::min(min.total, stats.total); + + max.open = std::max(max.open, stats.open); + max.close = std::max(max.close, stats.close); + max.readOrWrite = std::max(max.readOrWrite, stats.readOrWrite); + max.getInstance = std::max(max.getInstance, stats.getInstance); + max.release = std::max(max.release, stats.release); + max.openReadOrWriteClose = std::max(max.openReadOrWriteClose, stats.openReadOrWriteClose); + max.total = std::max(max.total, stats.total); + + total.open += stats.open; + total.close += stats.close; + total.readOrWrite += stats.readOrWrite; + total.getInstance += stats.getInstance; + total.release += stats.release; + total.openReadOrWriteClose += stats.openReadOrWriteClose; + total.total += stats.total; +} + +void averageStats(Stats& total, int count) { + total.open = std::round((double)total.open / count); + total.close = std::round((double)total.close / count); + total.readOrWrite = std::round((double)total.readOrWrite / count); + total.getInstance = std::round((double)total.getInstance / count); + total.release = std::round((double)total.release / count); + total.openReadOrWriteClose = std::round((double)total.openReadOrWriteClose / count); + total.total = std::round((double)total.total / count); +} + +void printStatsRow(const char* desc, system_tick_t min, system_tick_t max, system_tick_t avg) { + const auto w = MIN_MAX_AVG_COLUMN_WIDTH; + LOG_PRINTF(INFO, "%-*s%-*u%-*u%-*u\r\n", DESCR_COLUMN_WIDTH, desc, w, (unsigned)avg, w, (unsigned)max, w, (unsigned)min); +} + +void printStats(const Stats& min, const Stats& max, const Stats& avg, bool reading) { + const auto w = MIN_MAX_AVG_COLUMN_WIDTH; + LOG_PRINT(INFO, "\033[2m"); + LOG_PRINTF(INFO, "%-*s%-*s%-*s%-*s\r\n", DESCR_COLUMN_WIDTH, "", w, "avg", w, "max", w, "min"); + // ledger_get_instance + printStatsRow("ledger_get_instance", min.getInstance, max.getInstance, avg.getInstance); + // ledger_open + printStatsRow("ledger_open", min.open, max.open, avg.open); + // ledger_read/ledger_write + auto readOrWriteFn = reading ? "ledger_read" : "ledger_write"; + printStatsRow(readOrWriteFn, min.readOrWrite, max.readOrWrite, avg.readOrWrite); + // ledger_close + printStatsRow("ledger_close", min.close, max.close, avg.close); + // ledger_release + printStatsRow("ledger_release", min.release, max.release, avg.release); + LOG_PRINT(INFO, "\033[0m"); + // ledger_open + ledger_read/ledger_write + ledger_close + // This is a typical scenario for applications that instantiate ledgers globally + LOG_PRINT(INFO, "\033[1m"); + auto desc = String::format("ledger_open + %s + ledger_close", readOrWriteFn); + printStatsRow(desc, min.openReadOrWriteClose, max.openReadOrWriteClose, avg.openReadOrWriteClose); + // ledger_get_instance + ledger_open + ledger_read/ledger_write + ledger_close + ledger_release + // This is a typical scenario for applications that instantiate ledgers on demand + printStatsRow("ledger_get_instance + ledger_open + ... + ledger_release", min.total, max.total, avg.total); + LOG_PRINT(INFO, "\033[0m"); +} + +int testWriteAndRead(size_t size, bool flushOnRead = false) { + if (size > LARGE_DATA_SIZE) { + return Error::INTERNAL; + } + + CHECK(ledger_purge(LEDGER_NAME, nullptr)); + + // Pre-initialize the ledger directory + ledger_instance* ledger = nullptr; + CHECK(ledger_get_instance(&ledger, LEDGER_NAME, nullptr)); + NAMED_SCOPE_GUARD(releaseLedgerGuard, { + ledger_release(ledger, nullptr); + }); + if (!flushOnRead) { + releaseLedgerGuard.dismiss(); + ledger_release(ledger, nullptr); + } + + Stats readMin = { .open = MAX_TICKS, .close = MAX_TICKS, .readOrWrite = MAX_TICKS, .getInstance = MAX_TICKS, + .release = MAX_TICKS, .openReadOrWriteClose = MAX_TICKS, .total = MAX_TICKS }; + Stats readMax = {}; + Stats readAvg = {}; + + Stats writeMin = { .open = MAX_TICKS, .close = MAX_TICKS, .readOrWrite = MAX_TICKS, .getInstance = MAX_TICKS, + .release = MAX_TICKS, .openReadOrWriteClose = MAX_TICKS, .total = MAX_TICKS }; + Stats writeMax = {}; + Stats writeAvg = {}; + + Random rand; + + for (int i = 0; i < REPEAT_COUNT; ++i) { + delay(rand.gen() % 50); + + ledger_stream* stream = nullptr; + NAMED_SCOPE_GUARD(closeStreamGuard, { + int r = ledger_close(stream, 0, nullptr); // Can be called with a null stream + if (r < 0) { + LOG(ERROR, "ledger_close() failed: %d", r); + } + }); + if (flushOnRead) { + // Start reading the ledger using a separate stream so that the data written below can't + // be flushed immediately + CHECK(ledger_open(&stream, ledger, LEDGER_STREAM_MODE_READ, nullptr)); + } + + // Write data + rand.gen(outBuffer, size); + Stats writeStats = {}; + CHECK(writeToLedger(outBuffer, size, writeStats)); + + // Read the data back + Stats readStats = {}; + CHECK(readFromLedger(inBuffer, size, readStats, [&]() { + // Close the first stream so that the data can be flushed when reading is finished + if (flushOnRead) { + closeStreamGuard.dismiss(); + CHECK(ledger_close(stream, 0, nullptr)); + } + return 0; + })); + if (std::memcmp(inBuffer, outBuffer, size) != 0) { + LOG(ERROR, "Unexpected ledger data"); + return Error::BAD_DATA; + } + + updateStats(writeStats, writeMin, writeMax, writeAvg); + updateStats(readStats, readMin, readMax, readAvg); + } + + averageStats(writeAvg, REPEAT_COUNT); + averageStats(readAvg, REPEAT_COUNT); + + auto msg = flushOnRead ? "flushing when reading is finished" : "flushing immediately"; + LOG_PRINTF(INFO, "\r\n\033[4m%d writes of %d bytes; %s\033[0m:\r\n", REPEAT_COUNT, (int)size, msg); + printStats(writeMin, writeMax, writeAvg, false); + + LOG_PRINTF(INFO, "\r\n\033[4m%d reads of %d bytes; %s\033[0m:\r\n", REPEAT_COUNT, (int)size, msg); + printStats(readMin, readMax, readAvg, true /* reading */); + + return 0; +} + +int runTests() { + CHECK(ledger_purge_all(nullptr)); + + const int sizeCount = 3; + const size_t sizes[sizeCount] = { SMALL_DATA_SIZE, MEDIUM_DATA_SIZE, LARGE_DATA_SIZE }; + + const int testCount = sizeCount * 2; + int testIndex = 1; + + for (int i = 0; i < sizeCount; ++i) { + LOG_PRINTF(INFO, "\r\nRunning test %d of %d...\r\n", testIndex++, testCount); + CHECK(testWriteAndRead(sizes[i])); + + LOG_PRINTF(INFO, "\r\nRunning test %d of %d...\r\n", testIndex++, testCount); + CHECK(testWriteAndRead(sizes[i], true /* flushOnRead */)); + } + + CHECK(ledger_purge_all(nullptr)); + + LOG_PRINT(INFO, "\r\nDone.\r\n"); + + return 0; +} + +} // namespace + +void setup() { + waitUntil(Serial.isConnected); + delay(1000); + + int r = runTests(); + if (r < 0) { + LOG(ERROR, "runTests() failed: %d", r); + } +} + +void loop() { +} diff --git a/user/tests/app/ledger_test/README.md b/user/tests/app/ledger_test/README.md new file mode 100644 index 0000000000..efd2baad81 --- /dev/null +++ b/user/tests/app/ledger_test/README.md @@ -0,0 +1,51 @@ +# Ledger Test + +This test application provides a control request interface for managing ledgers stored on the device. It is accompanied with a command line utility that provides a user interface for the functionality exposed by the application. + +## Building + +The application is built and flashed like any other test application: +```sh +cd path/to/device-os/main +make -s all program-dfu PLATFORM=boron TEST=app/ledger_test +``` + +## Installation + +The command line utility is internal to this repository and can be executed directly from the source tree. In this case, the package dependencies need to be installed: +```sh +cd path/to/device-os/user/tests/app/ledger_test/cli +npm install +./ledger --help +``` + +Alternatively, `npm link` can be used to create a global symlink to the utility: +```sh +npm link path/to/device-os/user/tests/app/ledger_test/cli +ledger --help +``` + +## Usage + +Make sure a Particle device running the test application is connected to the computer via USB. Note that the command line utility does not support interacting with multiple devices and expects exactly one device to be connected to the computer. + +Enumerating the ledgers stored on the device: +```sh +ledger list +``` + +Setting the contents of a ledger: +```sh +ledger set my_ledger '{ "key1": "value1", "key2": 123 }' +``` + +Getting the contents of a ledger: +```sh +ledger get my_ledger +``` + +See `ledger --help` for the full list of commands supported by the application. + +## Debugging + +Logging output is printed to the device's USB serial interface. The logging level can be changed at run time using the `debug` command. diff --git a/user/tests/app/ledger_test/app/config.cpp b/user/tests/app/ledger_test/app/config.cpp new file mode 100644 index 0000000000..9320e98680 --- /dev/null +++ b/user/tests/app/ledger_test/app/config.cpp @@ -0,0 +1,37 @@ +#include + +#include +#include // For `retained` + +#include "config.h" + +namespace particle::test { + +namespace { + +retained Config g_config; +retained uint32_t g_magic; + +} // namespace + +void Config::setRestoreConnectionFlag() { + wasConnected = Particle.connected(); + restoreConnection = true; +} + +Config& Config::get() { + if (g_magic != 0xcf9addedu) { + g_config = { + .autoConnect = true, + .restoreConnection = false, + .wasConnected = false, + .removeLedger = false, + .removeAllLedgers = false, + .debugEnabled = false + }; + g_magic = 0xcf9addedu; + } + return g_config; +} + +} // namespace particle::test diff --git a/user/tests/app/ledger_test/app/config.h b/user/tests/app/ledger_test/app/config.h new file mode 100644 index 0000000000..8ed1e1ef2e --- /dev/null +++ b/user/tests/app/ledger_test/app/config.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +namespace particle::test { + +struct Config { + bool autoConnect; + bool restoreConnection; + bool wasConnected; + bool removeLedger; + bool removeAllLedgers; + bool debugEnabled; + + char removeLedgerName[LEDGER_MAX_NAME_LENGTH + 1]; + + void setRestoreConnectionFlag(); + + static Config& get(); +}; + +} // namespace particle::test diff --git a/user/tests/app/ledger_test/app/logger.cpp b/user/tests/app/ledger_test/app/logger.cpp new file mode 100644 index 0000000000..c9ca451136 --- /dev/null +++ b/user/tests/app/ledger_test/app/logger.cpp @@ -0,0 +1,80 @@ +#include +#include + +#include +#include +#include + +#include "logger.h" +#include "config.h" + +namespace particle::test { + +namespace { + +const auto LEDGER_CATEGORY = "system.ledger"; +const auto APP_CATEGORY = "app"; + +class AnsiLogHandler: public StreamLogHandler { +public: + using StreamLogHandler::StreamLogHandler; + +protected: + using StreamLogHandler::write; + + void logMessage(const char* msg, LogLevel level, const char* category, const LogAttributes& attr) override { + if (level >= LOG_LEVEL_ERROR) { + stream()->write("\033[31;1m"); // Red, bold + } else if (level >= LOG_LEVEL_WARN) { + stream()->write("\033[33;1m"); // Yellow, bold + } else if (category && std::strcmp(category, APP_CATEGORY) == 0) { + stream()->write("\033[32m"); // Green + } else if (category && std::strcmp(category, LEDGER_CATEGORY) == 0) { + stream()->write("\033[37m"); // White + } else { + stream()->write("\033[90m"); // Gray + } + writingMsg_ = true; + StreamLogHandler::logMessage(msg, level, category, attr); + writingMsg_ = false; + stream()->write("\033[0m"); // Reset + } + + void write(const char* data, size_t size) override { + if (!writingMsg_) { + stream()->write("\033[90m"); // Gray + } + StreamLogHandler::write(data, size); + if (!writingMsg_) { + stream()->write("\033[0m"); // Reset + } + } + +private: + bool writingMsg_ = false; +}; + +std::unique_ptr g_logHandler; + +} // namespace + +int initLogger() { + if (g_logHandler) { + LogManager::instance()->removeHandler(g_logHandler.get()); + g_logHandler.reset(); + } + auto& conf = Config::get(); + auto appLevel = conf.debugEnabled ? LOG_LEVEL_ALL : LOG_LEVEL_INFO; + auto defaultLevel = conf.debugEnabled ? LOG_LEVEL_ALL : LOG_LEVEL_WARN; + std::unique_ptr handler(new(std::nothrow) AnsiLogHandler(Serial, defaultLevel, { + { LEDGER_CATEGORY, appLevel }, + { APP_CATEGORY, appLevel } + })); + if (!handler || !LogManager::instance()->addHandler(handler.get())) { + return Error::NO_MEMORY; + } + g_logHandler = std::move(handler); + return 0; +} + +} // namespace particle::test diff --git a/user/tests/app/ledger_test/app/logger.h b/user/tests/app/ledger_test/app/logger.h new file mode 100644 index 0000000000..9c290f2585 --- /dev/null +++ b/user/tests/app/ledger_test/app/logger.h @@ -0,0 +1,7 @@ +#pragma once + +namespace particle::test { + +int initLogger(); + +} // namespace particle::test diff --git a/user/tests/app/ledger_test/app/main.cpp b/user/tests/app/ledger_test/app/main.cpp new file mode 100644 index 0000000000..7a19295b0e --- /dev/null +++ b/user/tests/app/ledger_test/app/main.cpp @@ -0,0 +1,76 @@ +#include + +#include "request_handler.h" +#include "logger.h" +#include "config.h" + +PRODUCT_VERSION(1) + +SYSTEM_MODE(SEMI_AUTOMATIC) +// SYSTEM_THREAD(ENABLED) + +using namespace particle::test; + +namespace { + +RequestHandler g_reqHandler; + +void onCloudStatus(system_event_t /* event */, int status) { + switch (status) { + case cloud_status_disconnected: { + Log.info("Disconnected"); + break; + } + case cloud_status_connecting: { + Log.info("Connecting"); + break; + } + case cloud_status_connected: { + Log.info("Connected"); + break; + } + case cloud_status_disconnecting: { + Log.info("Disconnecting"); + break; + } + default: + break; + } +} + +} // namespace + +void ctrl_request_custom_handler(ctrl_request* req) { + g_reqHandler.handleRequest(req); +} + +void setup() { + waitFor(Serial.isConnected, 3000); + int r = initLogger(); + SPARK_ASSERT(r == 0); + auto& conf = Config::get(); + if (conf.removeAllLedgers) { + conf.removeAllLedgers = false; + Log.info("Removing all ledgers"); + r = ledger_purge_all(nullptr); + if (r < 0) { + Log.error("ledger_purge_all() failed: %d", r); + } + } + if (conf.removeLedger) { + conf.removeLedger = false; + Log.info("Removing ledger: %s", conf.removeLedgerName); + r = ledger_purge(conf.removeLedgerName, nullptr); + if (r < 0) { + Log.error("ledger_purge() failed: %d", r); + } + } + System.on(cloud_status, onCloudStatus); + if ((!conf.restoreConnection && conf.autoConnect) || (conf.restoreConnection && conf.wasConnected)) { + Particle.connect(); + } + conf.restoreConnection = false; +} + +void loop() { +} diff --git a/user/tests/app/ledger_test/app/request_handler.cpp b/user/tests/app/ledger_test/app/request_handler.cpp new file mode 100644 index 0000000000..6c9ffe072a --- /dev/null +++ b/user/tests/app/ledger_test/app/request_handler.cpp @@ -0,0 +1,574 @@ +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include "request_handler.h" +#include "logger.h" +#include "config.h" + +namespace particle::test { + +namespace { + +const size_t LEDGER_READ_BLOCK_SIZE = 1024; + +enum Result { + RESET_PENDING = 1 +}; + +enum BinaryRequestType { + READ = 1, + WRITE = 2 +}; + +struct LedgerAppData { +}; + +// Completion handler for system_ctrl_set_result() +void systemResetCompletionHandler(int result, void* data) { + HAL_Delay_Milliseconds(1000); + System.reset(); +} + +void destroyLedgerAppData(void* appData) { + auto d = static_cast(appData); + delete d; +} + +int getLedgerInfo(ledger_info& info, ledger_instance* ledger) { + int r = ledger_get_info(ledger, &info, nullptr); + if (r < 0) { + Log.error("ledger_get_info() failed: %d", r); + } + return r; +} + +int getLedgerNames(char**& names, size_t& count) { + int r = ledger_get_names(&names, &count, nullptr); + if (r < 0) { + Log.error("ledger_get_names() failed: %d", r); + } + return r; +} + +int openLedgerStream(ledger_stream*& stream, ledger_instance* ledger, int mode) { + int r = ledger_open(&stream, ledger, mode, nullptr); + if (r < 0) { + Log.error("ledger_open() failed: %d", r); + } + return r; +} + +int closeLedgerStream(ledger_stream* stream, int flags = 0) { + int r = ledger_close(stream, flags, nullptr); + if (r < 0) { + Log.error("ledger_close() failed: %d", r); + } + return r; +} + +int readLedgerStream(ledger_stream* stream, char* data, size_t size) { + int r = ledger_read(stream, data, size, nullptr); + if (r < 0) { + Log.error("ledger_read() failed: %d", r); + } + return r; +} + +int writeLedgerStream(ledger_stream* stream, const char* data, size_t size) { + int r = ledger_write(stream, data, size, nullptr); + if (r < 0) { + Log.error("ledger_write() failed: %d", r); + } + return r; +} + +void ledgerSyncCallback(ledger_instance* ledger, void* appData) { + ledger_info info = {}; + info.version = LEDGER_API_VERSION; + int r = getLedgerInfo(info, ledger); + if (r < 0) { + return; + } + Log.info("Ledger synchronized: %s", info.name); +} + +int getLedger(ledger_instance*& ledger, const char* name) { + ledger_instance* lr = nullptr; + int r = ledger_get_instance(&lr, name, nullptr); + if (r < 0) { + Log.error("ledger_get_instance() failed: %d", r); + return r; + } + ledger_lock(lr, nullptr); + SCOPE_GUARD({ + ledger_unlock(lr, nullptr); + }); + auto appData = static_cast(ledger_get_app_data(lr, nullptr)); + if (!appData) { + appData = new(std::nothrow) LedgerAppData(); + if (!appData) { + return Error::NO_MEMORY; + } + ledger_callbacks callbacks = {}; + callbacks.version = LEDGER_API_VERSION; + callbacks.sync = ledgerSyncCallback; + ledger_set_callbacks(lr, &callbacks, nullptr); + ledger_set_app_data(lr, appData, destroyLedgerAppData, nullptr); + ledger_add_ref(lr, nullptr); // Keep the instance around + } + ledger = lr; + return 0; +} + +} // namespace + +class RequestHandler::JsonRequest { +public: + JsonRequest() : + req_(nullptr) { + } + + int init(ctrl_request* req) { + auto d = JSONValue::parse(req->request_data, req->request_size); + if (!d.isObject()) { + return Error::BAD_DATA; + } + data_ = std::move(d); + req_ = req; + return 0; + } + + template + int response(F fn) { + if (!req_) { + return Error::INVALID_STATE; + } + JSONBufferWriter writer(nullptr, 0); + fn(writer); + size_t size = writer.dataSize(); + CHECK(system_ctrl_alloc_reply_data(req_, size, nullptr)); + writer = JSONBufferWriter(req_->reply_data, req_->reply_size); + fn(writer); + if (writer.dataSize() != size) { + return Error::INTERNAL; + } + return 0; + } + + JSONValue get(const char* name) const { + JSONObjectIterator it(data_); + while (it.next()) { + if (it.name() == name) { + return it.value(); + } + } + return JSONValue(); + } + + bool has(const char* name) const { + return get(name).isValid(); + } + +private: + JSONValue data_; + ctrl_request* req_; +}; + +class RequestHandler::BinaryRequest { +public: + BinaryRequest() : + req_(nullptr), + data_(nullptr), + size_(0), + type_(0) { + } + + int init(ctrl_request* req) { + if (req->request_size < 4) { + return Error::BAD_DATA; + } + uint32_t type = 0; + std::memcpy(&type, req->request_data, 4); + type_ = bigEndianToNative(type); + data_ = req->request_data + 4; + size_ = req->request_size - 4; + req_ = req; + return 0; + } + + int allocResponse(char*& data, size_t size) { + CHECK(system_ctrl_alloc_reply_data(req_, size, nullptr)); + data = req_->reply_data; + return 0; + } + + void responseSize(size_t size) { + req_->reply_size = size; + } + + const char* data() const { + return data_; + } + + size_t size() const { + return size_; + } + + int type() const { + return type_; + } + +private: + ctrl_request* req_; + const char* data_; + size_t size_; + int type_; +}; + +RequestHandler::~RequestHandler() { + if (ledgerStream_) { + closeLedgerStream(ledgerStream_, LEDGER_STREAM_CLOSE_DISCARD); + } +} + +void RequestHandler::handleRequest(ctrl_request* req) { + int r = handleRequestImpl(req); + if (r < 0) { + Log.error("Error while handling control request: %d", r); + system_ctrl_alloc_reply_data(req, 0 /* size */, nullptr /* reserved */); + system_ctrl_set_result(req, r, nullptr /* handler */, nullptr /* data */, nullptr /* reserved */); + } +} + +int RequestHandler::handleRequestImpl(ctrl_request* req) { + Log.trace("Received request"); + auto size = req->request_size; + if (!size) { + return Error::NOT_ENOUGH_DATA; + } + int result = 0; + auto data = req->request_data; + if (data[0] == '{') { + Log.write(LOG_LEVEL_TRACE, data, size); + Log.print(LOG_LEVEL_TRACE, "\r\n"); + JsonRequest jsonReq; + CHECK(jsonReq.init(req)); + result = CHECK(handleJsonRequest(jsonReq)); + Log.trace("Sending response"); + if (req->reply_size > 0) { + Log.write(LOG_LEVEL_TRACE, req->reply_data, req->reply_size); + Log.print(LOG_LEVEL_TRACE, "\r\n"); + } + } else { + if (size > 0) { + Log.dump(LOG_LEVEL_TRACE, data, size); + Log.printf(LOG_LEVEL_TRACE, " (%u bytes)\r\n", (unsigned)size); + } + BinaryRequest binReq; + CHECK(binReq.init(req)); + result = CHECK(handleBinaryRequest(binReq)); + Log.trace("Sending response"); + if (req->reply_size > 0) { + Log.dump(LOG_LEVEL_TRACE, req->reply_data, req->reply_size); + Log.printf(LOG_LEVEL_TRACE, " (%u bytes)\r\n", (unsigned)req->reply_size); + } + } + auto handler = (result == Result::RESET_PENDING) ? systemResetCompletionHandler : nullptr; + system_ctrl_set_result(req, result, handler, nullptr /* data */, nullptr /* reserved */); + return 0; +} + +int RequestHandler::handleJsonRequest(JsonRequest& req) { + auto cmd = req.get("cmd").toString(); + if (cmd == "get") { + return get(req); + } else if (cmd == "set") { + return set(req); + } else if (cmd == "touch") { + return touch(req); + } else if (cmd == "list") { + return list(req); + } else if (cmd == "info") { + return info(req); + } else if (cmd == "reset") { + return reset(req); + } else if (cmd == "remove") { + return remove(req); + } else if (cmd == "connect") { + return connect(req); + } else if (cmd == "disconnect") { + return disconnect(req); + } else if (cmd == "auto_connect") { + return autoConnect(req); + } else if (cmd == "debug") { + return debug(req); + } else { + Log.error("Unknown command: \"%s\"", cmd.data()); + return Error::NOT_SUPPORTED; + } +} + +int RequestHandler::handleBinaryRequest(BinaryRequest& req) { + switch (req.type()) { + case BinaryRequestType::READ: + return read(req); + case BinaryRequestType::WRITE: + return write(req); + default: + Log.error("Unknown command: %d", req.type()); + return Error::NOT_SUPPORTED; + } +} + +int RequestHandler::get(JsonRequest& req) { + auto name = req.get("name").toString(); + Log.info("Getting ledger data: %s", name.data()); + if (ledgerStream_) { + Log.warn("\"get\" or \"set\" command is already in progress"); + CHECK(closeLedgerStream(ledgerStream_, LEDGER_STREAM_CLOSE_DISCARD)); + ledgerStream_ = nullptr; + } + ledger_instance* ledger = nullptr; + CHECK(getLedger(ledger, name.data())); + ledger_lock(ledger, nullptr); + SCOPE_GUARD({ + ledger_unlock(ledger, nullptr); + ledger_release(ledger, nullptr); + }); + ledger_info info = {}; + info.version = LEDGER_API_VERSION; + CHECK(getLedgerInfo(info, ledger)); + ledger_stream* stream = nullptr; + CHECK(openLedgerStream(stream, ledger, LEDGER_STREAM_MODE_READ)); + NAMED_SCOPE_GUARD(closeStreamGuard, { + closeLedgerStream(stream, LEDGER_STREAM_CLOSE_DISCARD); + }); + CHECK(req.response([&](auto& w) { + w.beginObject(); + w.name("size").value(info.data_size); + w.endObject(); + })); + if (info.data_size > 0) { + ledgerStream_ = stream; + ledgerBytesLeft_ = info.data_size; + closeStreamGuard.dismiss(); + } + return 0; +} + +int RequestHandler::set(JsonRequest& req) { + auto name = req.get("name").toString(); + Log.info("Setting ledger data: %s", name.data()); + if (ledgerStream_) { + Log.warn("\"get\" or \"set\" command is already in progress"); + CHECK(closeLedgerStream(ledgerStream_, LEDGER_STREAM_CLOSE_DISCARD)); + ledgerStream_ = nullptr; + } + auto size = req.get("size").toInt(); + if (size < 0) { + Log.error("Invalid size of ledger data"); + return Error::BAD_DATA; + } + ledger_instance* ledger = nullptr; + CHECK(getLedger(ledger, name.data())); + SCOPE_GUARD({ + ledger_release(ledger, nullptr); + }); + ledger_stream* stream = nullptr; + CHECK(openLedgerStream(stream, ledger, LEDGER_STREAM_MODE_WRITE)); + if (size > 0) { + ledgerStream_ = stream; + ledgerBytesLeft_ = size; + } else { + CHECK(closeLedgerStream(stream)); + } + return 0; +} + +int RequestHandler::touch(JsonRequest& req) { + auto name = req.get("name").toString(); + Log.info("Getting ledger instance: %s", name.data()); + ledger_instance* ledger = nullptr; + CHECK(getLedger(ledger, name.data())); + ledger_release(ledger, nullptr); + return 0; +} + +int RequestHandler::list(JsonRequest& req) { + Log.info("Enumerating ledgers"); + char** names = nullptr; + size_t count = 0; + CHECK(getLedgerNames(names, count)); + SCOPE_GUARD({ + for (size_t i = 0; i < count; ++i) { + std::free(names[i]); + } + std::free(names); + }); + CHECK(req.response([&](auto& w) { + w.beginArray(); + for (size_t i = 0; i < count; ++i) { + w.value(names[i]); + } + w.endArray(); + })); + return 0; +} + +int RequestHandler::info(JsonRequest& req) { + auto name = req.get("name").toString(); + Log.info("Getting ledger info: %s", name.data()); + ledger_instance* ledger = nullptr; + CHECK(getLedger(ledger, name.data())); + SCOPE_GUARD({ + ledger_release(ledger, nullptr); + }); + ledger_info info = {}; + info.version = LEDGER_API_VERSION; + CHECK(getLedgerInfo(info, ledger)); + CHECK(req.response([&](auto& w) { + w.beginObject(); + w.name("last_updated").value(info.last_updated); + w.name("last_synced").value(info.last_synced); + w.name("data_size").value(info.data_size); + w.name("scope").value(info.scope); + w.name("sync_direction").value(info.sync_direction); + w.endObject(); + })); + return 0; +} + +int RequestHandler::reset(JsonRequest& req) { + Log.info("Resetting device"); + return Result::RESET_PENDING; +} + +int RequestHandler::remove(JsonRequest& req) { + auto& conf = Config::get(); + if (conf.removeLedger || conf.removeAllLedgers) { + Log.warn("\"remove\" command is already in progress"); + } + auto removeAll = req.get("all").toBool(); + if (removeAll) { + conf.removeAllLedgers = true; + } else { + auto name = req.get("name").toString(); + if (name.isEmpty()) { + Log.error("Ledger name is missing"); + return Error::BAD_DATA; + } + size_t n = strlcpy(conf.removeLedgerName, name.data(), sizeof(conf.removeLedgerName)); + if (n >= sizeof(conf.removeLedgerName)) { + Log.error("Ledger name is too long"); + return Error::BAD_DATA; + } + conf.removeLedger = true; + } + conf.setRestoreConnectionFlag(); + return Result::RESET_PENDING; +} + +int RequestHandler::connect(JsonRequest& req) { + Particle.connect(); + return 0; +} + +int RequestHandler::disconnect(JsonRequest& req) { + Network.off(); + return 0; +} + +int RequestHandler::autoConnect(JsonRequest& req) { + auto enabled = req.get("enabled"); + auto& conf = Config::get(); + conf.autoConnect = enabled.isValid() ? enabled.toBool() : true; + if (conf.autoConnect) { + Log.info("Enabled auto-connect"); + Particle.connect(); + } else { + Log.info("Disabled auto-connect"); + } + return 0; +} + +int RequestHandler::debug(JsonRequest& req) { + auto enabled = req.get("enabled"); + auto& conf = Config::get(); + conf.debugEnabled = enabled.isValid() ? enabled.toBool() : true; + if (conf.debugEnabled) { + Log.info("Enabled debug"); + } else { + Log.info("Disabled debug"); + } + CHECK(initLogger()); + return 0; +} + +int RequestHandler::read(BinaryRequest& req) { + int r = readImpl(req); + if (r < 0 && ledgerStream_) { + closeLedgerStream(ledgerStream_, LEDGER_STREAM_CLOSE_DISCARD); + ledgerStream_ = nullptr; + } + return r; +} + +int RequestHandler::readImpl(BinaryRequest& req) { + if (!ledgerStream_) { + Log.error("Ledger is not open"); + return Error::INVALID_STATE; + } + char* data = nullptr; + CHECK(req.allocResponse(data, LEDGER_READ_BLOCK_SIZE)); + size_t n = std::min(ledgerBytesLeft_, LEDGER_READ_BLOCK_SIZE); + CHECK(readLedgerStream(ledgerStream_, data, n)); + req.responseSize(n); + ledgerBytesLeft_ -= n; + if (!ledgerBytesLeft_) { + closeLedgerStream(ledgerStream_); // Don't use CHECK here + ledgerStream_ = nullptr; + } + return 0; +} + +int RequestHandler::write(BinaryRequest& req) { + int r = writeImpl(req); + if (r < 0 && ledgerStream_) { + closeLedgerStream(ledgerStream_, LEDGER_STREAM_CLOSE_DISCARD); + ledgerStream_ = nullptr; + } + return r; +} + +int RequestHandler::writeImpl(BinaryRequest& req) { + if (!ledgerStream_) { + Log.error("Ledger is not open"); + return Error::INVALID_STATE; + } + if (req.size() > ledgerBytesLeft_) { + Log.error("Unexpected size of ledger data"); + return Error::TOO_LARGE; + } + CHECK(writeLedgerStream(ledgerStream_, req.data(), req.size())); + ledgerBytesLeft_ -= req.size(); + if (!ledgerBytesLeft_) { + closeLedgerStream(ledgerStream_); // Don't use CHECK here + ledgerStream_ = nullptr; + } + return 0; +} + +} // namespace particle::test diff --git a/user/tests/app/ledger_test/app/request_handler.h b/user/tests/app/ledger_test/app/request_handler.h new file mode 100644 index 0000000000..f497ec124f --- /dev/null +++ b/user/tests/app/ledger_test/app/request_handler.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +namespace particle::test { + +class RequestHandler { +public: + RequestHandler() : + ledgerStream_(nullptr), + ledgerBytesLeft_(0) { + } + + ~RequestHandler(); + + void handleRequest(ctrl_request* req); + +private: + class JsonRequest; + class BinaryRequest; + + ledger_stream* ledgerStream_; + size_t ledgerBytesLeft_; + + int handleRequestImpl(ctrl_request* req); + int handleJsonRequest(JsonRequest& req); + int handleBinaryRequest(BinaryRequest& req); + + int get(JsonRequest& req); + int set(JsonRequest& req); + int touch(JsonRequest& req); + int list(JsonRequest& req); + int info(JsonRequest& req); + int reset(JsonRequest& req); + int remove(JsonRequest& req); + int connect(JsonRequest& req); + int disconnect(JsonRequest& req); + int autoConnect(JsonRequest& req); + int debug(JsonRequest& req); + + int read(BinaryRequest& req); + int readImpl(BinaryRequest& req); + int write(BinaryRequest& req); + int writeImpl(BinaryRequest& req); +}; + +} // namespace particle::test diff --git a/user/tests/app/ledger_test/build.mk b/user/tests/app/ledger_test/build.mk new file mode 100644 index 0000000000..cc42966a01 --- /dev/null +++ b/user/tests/app/ledger_test/build.mk @@ -0,0 +1 @@ +CPPSRC += $(call target_files,$(MODULE_PATH)/$(USRSRC)/app/,*.cpp) diff --git a/user/tests/app/ledger_test/cli/errors.js b/user/tests/app/ledger_test/cli/errors.js new file mode 100644 index 0000000000..e675d80074 --- /dev/null +++ b/user/tests/app/ledger_test/cli/errors.js @@ -0,0 +1,53 @@ +const fs = require('fs'); +const path = require('path'); + +const deviceOsErrors = (() => { + const headerFile = 'services/inc/system_error.h'; + try { + const src = fs.readFileSync(path.join(__dirname, '../../../../..', headerFile), 'utf-8'); + let r = src.match(/#define\s+SYSTEM_ERRORS\b.*?\n/); + if (!r) { + throw new Error(); + } + let line = r[0]; + let pos = r.index + line.length; + const lines = [line]; + while (/^.*?\\\s*?\n$/.test(line)) { // Ends with '\' followed by a newline + line = src.slice(pos, src.indexOf('\n', pos) + 1); + pos += line.length; + lines.push(line); + } + const errors = new Map(); + for (const line of lines) { + const matches = line.matchAll(/\(\s*(\w+)\s*,\s*"(.*?)"\s*,\s*(-\d+)\s*\)/g); + for (const m of matches) { + const name = m[1]; + const message = m[2]; + const code = Number.parseInt(m[3]); + if (Number.isNaN(code)) { + throw new Error(); + } + errors.set(code, { name, message }); + } + } + if (!errors.size) { + throw new Error(); + } + return errors; + } catch (err) { + console.error(`Failed to parse ${headerFile}`); + return new Map(); + } +})(); + +function deviceOsErrorCodeToString(code) { + const err = deviceOsErrors.get(code); + if (!err) { + return `Unknown error (${code})`; + } + return `${err.message} (${err.name})`; +} + +module.exports = { + deviceOsErrorCodeToString +}; diff --git a/user/tests/app/ledger_test/cli/ledger b/user/tests/app/ledger_test/cli/ledger new file mode 100755 index 0000000000..b7094141e3 --- /dev/null +++ b/user/tests/app/ledger_test/cli/ledger @@ -0,0 +1,424 @@ +#!/usr/bin/env node +const { randomObject, timestampToString } = require('./util'); +const { deviceOsErrorCodeToString } = require('./errors'); +const { name: PACKAGE_NAME, description: PACKAGE_DESC } = require('./package.json'); + +const { getDevices } = require('particle-usb'); +const cbor = require('cbor'); +const parseArgs = require('minimist'); +const _ = require('lodash'); + +const fs = require('fs'); + +const WRITE_BLOCK_SIZE = 1024; +const REQUEST_TYPE = 10; // ctrl_request_type::CTRL_REQUEST_APP_CUSTOM + +const BinaryRequestType = { + READ: 1, + WRITE: 2 +}; + +function printUsage() { + console.log(`\ +${PACKAGE_DESC} + +Usage: + ${PACKAGE_NAME} [arguments...] + +Commands: + get [--expect[=]] + Get ledger data. + set [|--size=] + Set ledger data. \`data\` can be a filename or string containing a JSON document. If neither + \`data\` nor \`size\` is provided, the data is read from the standard input. + touch + Create a ledger if it doesn't exist. + info [] [--raw] + Get ledger info. If \`name\` is not provided, returns the info for all ledgers. + list [--raw] + List the ledgers. + remove |--all + Remove a specific ledger or all ledgers. + connect + Connect to the Cloud. + disconnect + Disconnect from the Cloud. + auto-connect [1|0] + Enable/disable automatic connection to the Cloud. + debug [1|0] + Enable/disable debug logging. + reset + Reset the device. + gen + Generate and print random ledger data of \`size\` bytes. + +Options: + --expect[=] + Exit with an error if the ledger data doesn't match the expected data. \`data\` can be a + filename or JSON string. If not specified, the data is read from the standard input. + --size=, -n + Generate random ledger data of \`size\` bytes. + --all + Run the command for all ledgers. + --raw + Output the raw response data received from the device.`); +} + +function scopeName(scope) { + switch (scope) { + case 0: return 'Unknown'; + case 1: return 'Device'; + case 2: return 'Product'; + case 3: return 'Owner'; + default: return `Unknown (${scope})`; + } +} + +function syncDirectionName(dir) { + switch (dir) { + case 0: return 'Unknown'; + case 1: return 'Device-to-cloud'; + case 2: return 'Cloud-to-device'; + default: return `Unknown (${dir})`; + } +} + +function readJsonObject(arg) { + let data; + try { + if (!arg) { + data = fs.readFileSync(0, 'utf8'); + } else if (arg.trimStart().startsWith('{')) { + data = arg; + } else { + data = fs.readFileSync(arg, 'utf8'); + } + } catch (err) { + throw new Error('Failed to load JSON', { cause: err }); + } + try { + data = JSON.parse(data); + } catch (err) { + throw new Error('Failed to parse JSON', { cause: err }); + } + if (!_.isPlainObject(data)) { + throw new Error('JSON is not an object'); + } + return data; +} + +async function openDevice() { + const devs = await getDevices(); + if (!devs.length) { + throw new Error('No devices found'); + } + if (devs.length !== 1) { + throw new Error('Multiple devices found'); + } + const dev = devs[0]; + await dev.open(); + return dev; +} + +async function sendRequest(dev, data) { + let resp; + try { + resp = await dev.sendControlRequest(REQUEST_TYPE, data); + } catch (err) { + throw new Error('Failed to send control request', { cause: err }); + } + if (resp.result < 0) { + const msg = deviceOsErrorCodeToString(resp.result); + throw new Error(msg); + } + return resp.data || null; +} + +async function sendJsonRequest(dev, data) { + data = Buffer.from(JSON.stringify(data)); + let resp = await sendRequest(dev, data); + if (resp) { + resp = JSON.parse(resp.toString()); + } + return resp; +} + +async function sendBinaryRequest(dev, type, data) { + let buf = Buffer.alloc(4); + buf.writeUInt32BE(type, 0); + if (data) { + buf = Buffer.concat([buf, data]); + } + return await sendRequest(dev, buf); +} + +async function get(args, opts, dev) { + if (!args.length) { + throw new Error('Missing ledger name'); + } + let resp = await sendJsonRequest(dev, { + cmd: 'get', + name: args[0] + }); + const size = resp.size; + let data = Buffer.alloc(0); + while (data.length < size) { + resp = await sendBinaryRequest(dev, BinaryRequestType.READ); + data = Buffer.concat([data, resp]); + } + if (data.length) { + try { + data = await cbor.decodeFirst(data); + } catch (err) { + throw new Error('Failed to parse CBOR', { cause: err }); + } + } else { + data = {}; + } + if (opts.expect !== undefined) { + const expected = readJsonObject(opts.expect); + if (!_.isEqual(data, expected)) { + throw new Error('Ledger data doesn\'t match the expected data'); + } + console.error('Ledger data matches the expected data'); + } else { + console.log(JSON.stringify(data)); + } +} + +async function set(args, opts, dev) { + if (!args.length) { + throw new Error('Missing ledger name'); + } + let obj; + if (opts.size !== undefined) { + obj = await randomObject(opts.size); + } else { + obj = readJsonObject(args[1]); + } + const data = await cbor.encodeAsync(obj); + await sendJsonRequest(dev, { + cmd: 'set', + name: args[0], + size: data.length + }); + let offs = 0; + while (offs < data.length) { + const n = Math.min(data.length - offs, WRITE_BLOCK_SIZE); + await sendBinaryRequest(dev, BinaryRequestType.WRITE, data.slice(offs, offs + n)); + offs += n; + } + if (opts.size !== undefined) { + console.log(JSON.stringify(obj)); + } +} + +async function touch(args, opts, dev) { + if (!args.length) { + throw new Error('Missing ledger name'); + } + await sendJsonRequest(dev, { + cmd: 'touch', + name: args[0] + }); +} + +async function info(args, opts, dev) { + function pad(str) { + return str.padEnd(20); + } + + function formatInfo(info) { + return `\ +${pad('Scope:')}${scopeName(info.scope)} +${pad('Sync direction:')}${syncDirectionName(info.sync_direction)} +${pad('Data size:')}${info.data_size} +${pad('Last update:')}${info.last_updated ? timestampToString(info.last_updated) : 'N/A'} +${pad('Last sync:')}${info.last_synced ? timestampToString(info.last_synced) : 'N/A'}`; + } + + if (args.length > 0) { + const info = await sendJsonRequest(dev, { + cmd: 'info', + name: args[0] + }); + if (opts.raw) { + console.log(JSON.stringify(info)); + } else { + console.log(formatInfo(info)); + } + } else { + const ledgerNames = await sendJsonRequest(dev, { + cmd: 'list' + }); + const infoList = []; + for (const name of ledgerNames) { + const info = await sendJsonRequest(dev, { + cmd: 'info', + name + }); + info.name = name; + infoList.push(info); + } + if (opts.raw) { + console.log(JSON.stringify(infoList)); + } else { + let out = ''; + for (const info of infoList) { + out += `${pad('Name:')}${info.name}\n`; + out += formatInfo(info); + out += '\n\n'; + } + console.log(out.trimRight()); + } + } +} + +async function list(args, opts, dev) { + const resp = await sendJsonRequest(dev, { + cmd: 'list' + }); + if (opts.raw) { + console.log(JSON.stringify(resp)); + } else { + for (const name of resp) { + console.log(name); + } + } +} + +async function remove(args, opts, dev) { + const req = { + cmd: 'remove' + }; + if (opts.all) { + req.all = true; + } else { + if (!args.length) { + throw new Error('Missing ledger name'); + } + req.name = args[0]; + } + await sendJsonRequest(dev, req); +} + +async function connect(args, opts, dev) { + await sendJsonRequest(dev, { + cmd: 'connect' + }); +} + +async function disconnect(args, opts, dev) { + await sendJsonRequest(dev, { + cmd: 'disconnect' + }); +} + +async function autoConnect(args, opts, dev) { + let enabled = true; + if (args.length) { + enabled = !!Number.parseInt(args[0]); + } + await sendJsonRequest(dev, { + cmd: 'auto_connect', + enabled + }); +} + +async function debug(args, opts, dev) { + let enabled = true; + if (args.length) { + enabled = !!Number.parseInt(args[0]); + } + await sendJsonRequest(dev, { + cmd: 'debug', + enabled + }); +} + +async function reset(args, opts, dev) { + await sendJsonRequest(dev, { + cmd: 'reset' + }); +} + +async function gen(args, opts) { + if (!args.length) { + throw new Error('Missing data size'); + } + const size = Number.parseInt(args[0]); + if (Number.isNaN(size)) { + throw new Error('Invalid data size'); + } + const obj = await randomObject(size); + console.log(JSON.stringify(obj)); +} + +async function runCommand(cmd, args, opts) { + let fn; + let needDev = true; + switch (cmd) { + case 'get': fn = get; break; + case 'set': fn = set; break; + case 'touch': fn = touch; break; + case 'info': fn = info; break; + case 'list': fn = list; break; + case 'remove': fn = remove; break; + case 'connect': fn = connect; break; + case 'disconnect': fn = disconnect; break; + case 'auto-connect': fn = autoConnect; break; + case 'debug': fn = debug; break; + case 'reset': fn = reset; break; + case 'gen': fn = gen; needDev = false; break; + default: + throw new Error(`Unknown command: ${cmd}`) + } + let dev; + if (needDev) { + dev = await openDevice(); + } + try { + await fn(args, opts, dev); + } finally { + if (dev) { + await dev.close(); + } + } +} + +async function run() { + let ok = true; + try { + const opts = parseArgs(process.argv.slice(2), { + string: ['_', 'expect'], + boolean: ['all', 'raw', 'help'], + number: ['size'], + alias: { + 'help': 'h', + 'size': 'n' + }, + unknown: arg => { + if (arg.startsWith('-')) { + throw new RangeError(`Unknown argument: ${arg}`); + } + } + }); + if (opts.help) { + printUsage(); + } else { + const args = opts._; + delete opts._; + if (!args.length) { + throw new Error('Missing command name'); + } + const cmd = args.shift(); + await runCommand(cmd, args, opts); + } + } catch (err) { + console.error(err); + ok = false; + } + process.exit(ok ? 0 : 1); +} + +run(); diff --git a/user/tests/app/ledger_test/cli/npm-shrinkwrap.json b/user/tests/app/ledger_test/cli/npm-shrinkwrap.json new file mode 100644 index 0000000000..d4047f31ac --- /dev/null +++ b/user/tests/app/ledger_test/cli/npm-shrinkwrap.json @@ -0,0 +1,393 @@ +{ + "name": "ledger", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "ledger", + "dependencies": { + "cbor": "^9.0.1", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "particle-usb": "^2.4.1" + }, + "bin": { + "ledger": "ledger" + } + }, + "node_modules/@particle/device-constants": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@particle/device-constants/-/device-constants-3.3.0.tgz", + "integrity": "sha512-QAx7j77A2ADyVq/vtEzuhrbFM3JXn5gfmC6OpvCuFbLvGoGNcZHBdCaf+y1krrFG9PiWcezNEmhGdkIGtjGsWA==", + "peer": true, + "engines": { + "node": ">=12.x", + "npm": "8.x" + } + }, + "node_modules/@particle/device-os-protobuf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@particle/device-os-protobuf/-/device-os-protobuf-1.2.1.tgz", + "integrity": "sha512-Y1T7LkZ1LtNAM/DC4nF6kUKpr9DYYvBDRBZikyv0APup01qW8MaB0/Rzdf+4tumHRBKRCA3zFmPGz56ietnTgQ==", + "dependencies": { + "protobufjs": "^6.11.2" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "node_modules/@types/node": { + "version": "20.8.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.8.tgz", + "integrity": "sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ==", + "dependencies": { + "undici-types": "~5.25.1" + } + }, + "node_modules/@types/w3c-web-usb": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.9.tgz", + "integrity": "sha512-6EIpb9g9k/SGu59mQ6RW3tedmabtE+N3iGRa98+1CCFuhGt565wLEYKXoEVKTuNrCr2SrgfvBMN5db6hggkzKQ==" + }, + "node_modules/cbor": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.1.tgz", + "integrity": "sha512-/TQOWyamDxvVIv+DY9cOLNuABkoyz8K/F3QE56539pGVYohx0+MEA1f4lChFTX79dBTBS7R1PF6ovH7G+VtBfQ==", + "dependencies": { + "nofilter": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-addon-api": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", + "integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==" + }, + "node_modules/node-gyp-build": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", + "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", + "engines": { + "node": ">=12.19" + } + }, + "node_modules/particle-usb": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/particle-usb/-/particle-usb-2.4.1.tgz", + "integrity": "sha512-xzeVuxFeHcEKwSO9nVdGo7emMRW/pCbUc/Zm/ORCaetFIHWWNCP+7CMrwzrHTDlSvngftrJx2h+qbPqgoKM06Q==", + "dependencies": { + "@particle/device-os-protobuf": "^1.2.1", + "protobufjs": "^6.11.3", + "usb": "^2.11.0" + }, + "engines": { + "node": ">=12", + "npm": "8.x" + }, + "peerDependencies": { + "@particle/device-constants": "^3.1.8" + } + }, + "node_modules/protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, + "node_modules/undici-types": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", + "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==" + }, + "node_modules/usb": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.11.0.tgz", + "integrity": "sha512-u5+NZ6DtoW8TIBtuSArQGAZZ/K15i3lYvZBAYmcgI+RcDS9G50/KPrUd3CrU8M92ahyCvg5e0gc8BDvr5Hwejg==", + "hasInstallScript": true, + "dependencies": { + "@types/w3c-web-usb": "^1.0.6", + "node-addon-api": "^7.0.0", + "node-gyp-build": "^4.5.0" + }, + "engines": { + "node": ">=12.22.0 <13.0 || >=14.17.0" + } + } + }, + "dependencies": { + "@particle/device-constants": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@particle/device-constants/-/device-constants-3.3.0.tgz", + "integrity": "sha512-QAx7j77A2ADyVq/vtEzuhrbFM3JXn5gfmC6OpvCuFbLvGoGNcZHBdCaf+y1krrFG9PiWcezNEmhGdkIGtjGsWA==", + "peer": true + }, + "@particle/device-os-protobuf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@particle/device-os-protobuf/-/device-os-protobuf-1.2.1.tgz", + "integrity": "sha512-Y1T7LkZ1LtNAM/DC4nF6kUKpr9DYYvBDRBZikyv0APup01qW8MaB0/Rzdf+4tumHRBKRCA3zFmPGz56ietnTgQ==", + "requires": { + "protobufjs": "^6.11.2" + } + }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, + "@types/node": { + "version": "20.8.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.8.tgz", + "integrity": "sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ==", + "requires": { + "undici-types": "~5.25.1" + } + }, + "@types/w3c-web-usb": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.9.tgz", + "integrity": "sha512-6EIpb9g9k/SGu59mQ6RW3tedmabtE+N3iGRa98+1CCFuhGt565wLEYKXoEVKTuNrCr2SrgfvBMN5db6hggkzKQ==" + }, + "cbor": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-9.0.1.tgz", + "integrity": "sha512-/TQOWyamDxvVIv+DY9cOLNuABkoyz8K/F3QE56539pGVYohx0+MEA1f4lChFTX79dBTBS7R1PF6ovH7G+VtBfQ==", + "requires": { + "nofilter": "^3.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "node-addon-api": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", + "integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==" + }, + "node-gyp-build": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.1.tgz", + "integrity": "sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==" + }, + "nofilter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==" + }, + "particle-usb": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/particle-usb/-/particle-usb-2.4.1.tgz", + "integrity": "sha512-xzeVuxFeHcEKwSO9nVdGo7emMRW/pCbUc/Zm/ORCaetFIHWWNCP+7CMrwzrHTDlSvngftrJx2h+qbPqgoKM06Q==", + "requires": { + "@particle/device-os-protobuf": "^1.2.1", + "protobufjs": "^6.11.3", + "usb": "^2.11.0" + } + }, + "protobufjs": { + "version": "6.11.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", + "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + } + }, + "undici-types": { + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", + "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==" + }, + "usb": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.11.0.tgz", + "integrity": "sha512-u5+NZ6DtoW8TIBtuSArQGAZZ/K15i3lYvZBAYmcgI+RcDS9G50/KPrUd3CrU8M92ahyCvg5e0gc8BDvr5Hwejg==", + "requires": { + "@types/w3c-web-usb": "^1.0.6", + "node-addon-api": "^7.0.0", + "node-gyp-build": "^4.5.0" + } + } + } +} diff --git a/user/tests/app/ledger_test/cli/package.json b/user/tests/app/ledger_test/cli/package.json new file mode 100644 index 0000000000..d77600007a --- /dev/null +++ b/user/tests/app/ledger_test/cli/package.json @@ -0,0 +1,13 @@ +{ + "name": "ledger", + "description": "Ledger Test Utility", + "private": true, + "main": "ledger", + "bin": "ledger", + "dependencies": { + "cbor": "^9.0.1", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "particle-usb": "^2.4.1" + } +} diff --git a/user/tests/app/ledger_test/cli/util.js b/user/tests/app/ledger_test/cli/util.js new file mode 100644 index 0000000000..03af13442a --- /dev/null +++ b/user/tests/app/ledger_test/cli/util.js @@ -0,0 +1,105 @@ +const cbor = require('cbor'); + +function randomInt(min, max) { + if (min > max) { + const m = min; + min = max; + max = m; + } + return min + Math.floor(Math.random() * (max - min + 1)); +} + +function randomString(len) { + const alpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'; + let s = ''; + for (let i = 0; i < len; ++i) { + s += alpha.charAt(Math.floor(Math.random() * alpha.length)); + } + return s; +} + +async function randomObject(size) { + let obj = {}; + let objSize; + + async function set(key, val) { + obj[key] = val; + objSize = (await cbor.encodeAsync(obj)).length; // Synchronous variant of this function doesn't work with large objects + } + async function remove(key) { + delete obj[key]; + objSize = (await cbor.encodeAsync(obj)).length; + } + + // Start with a small non-empty object + let fillKey = randomString(1); + await set(fillKey, ''); + const minObjSize = objSize; + if (size < minObjSize) { + throw new Error(`Minimum data size is ${minObjSize} bytes`); + } + let minEntryLen; + let maxEntryLen; + if (size < 200) { + // Note that everything in this algorithm is completely arbitrary + minEntryLen = 5; + maxEntryLen = 20; + } else if (size < 2000) { + minEntryLen = 10; + maxEntryLen = 50; + } else { + minEntryLen = 30; + maxEntryLen = 120; + } + // Fill the object with random entries + let key; + do { + const entryLen = randomInt(minEntryLen, maxEntryLen); + const keyLen = randomInt(1, Math.ceil(entryLen / 2)); + do { + key = randomString(keyLen); + } while (key in obj); + let val; + const valLen = entryLen - keyLen; + if (valLen > minObjSize && Math.random() < 0.25) { + val = await randomObject(valLen); + } else { + val = randomString(valLen); + } + await set(key, val); + } while (objSize <= size); + await remove(key); + // Object size is at or somewhat below the requested size now + let fillVal = randomString(size - objSize); + await set(fillKey, fillVal); + if (objSize > size) { + // Encoding the value length required some extra bytes + fillVal = randomString(fillVal.length - (objSize - size)); + await set(fillKey, fillVal); + if (objSize < size) { + // The value length was encoded with fewer bytes again. The reserved key is 1 character long + // so extending it by a few characters is unlikely to exceed the requested size + await remove(fillKey); + do { + fillKey = randomString(size - objSize + 1); + } while (fillKey in obj); + await set(fillKey, fillVal); + } + } + // Sanity check + if (objSize != size) { + throw new Error('Failed to generate object of specified size'); + } + return obj; +} + +function timestampToString(time) { + return new Date(time).toISOString(); +} + +module.exports = { + randomInt, + randomString, + randomObject, + timestampToString +}; diff --git a/user/tests/integration/communication/ledger/ledger.cpp b/user/tests/integration/communication/ledger/ledger.cpp new file mode 100644 index 0000000000..d486180506 --- /dev/null +++ b/user/tests/integration/communication/ledger/ledger.cpp @@ -0,0 +1,183 @@ +#include +#include + +#include "application.h" +#include "unit-test/unit-test.h" + +PRODUCT_VERSION(1) + +#if Wiring_Ledger + +namespace { + +const auto DEVICE_TO_CLOUD_LEDGER = "test-device-to-cloud"; +const auto CLOUD_TO_DEVICE_LEDGER = "test-cloud-to-device"; + +#ifdef DEBUG_BUILD + +Serial1LogHandler g_logHandler(115200, LOG_LEVEL_WARN, { + { "system.ledger", LOG_LEVEL_ALL }, + { "wiring", LOG_LEVEL_ALL }, + { "app", LOG_LEVEL_ALL } +}); + +#endif // defined(DEBUG_BUILD) + +int g_seed = 0; +bool g_synced = false; + +} // namespace + +test(01_init_ledgers) { + // Remove local ledger data + assertEqual(Ledger::removeAll(), 0); + + auto ledger1 = Particle.ledger(DEVICE_TO_CLOUD_LEDGER); + assertTrue(ledger1.scope() == LedgerScope::UNKNOWN); + assertTrue(ledger1.isWritable()); + + auto ledger2 = Particle.ledger(CLOUD_TO_DEVICE_LEDGER); + assertTrue(ledger2.scope() == LedgerScope::UNKNOWN); + assertTrue(ledger2.isWritable()); // Actual sync direction is unknown initially so the ledger is writable + + Particle.connect(); + assertTrue(waitFor(Particle.connected, 60000)); + + // Let the system request the ledger info + delay(10000); + + assertTrue(ledger1.scope() == LedgerScope::DEVICE); + assertTrue(ledger1.isWritable()); + + assertTrue(ledger2.scope() == LedgerScope::DEVICE); + assertFalse(ledger2.isWritable()); // Ledger is no longer writable + + // Generate a seed value for randomizing ledger data + g_seed = rand() % 1000000 + 1; + pushMailboxMsg(String::format("%d", g_seed), 5000 /* wait */); +} + +test(02_sync_device_to_cloud) { + g_synced = false; + auto ledger = Particle.ledger(DEVICE_TO_CLOUD_LEDGER); + ledger.onSync([](Ledger /* ledger */) { + g_synced = true; + }); + SCOPE_GUARD({ + ledger.onSync(nullptr); + }); + LedgerData d = { { "a", g_seed + 1 } }; + ledger.set(d); + waitFor([]() { + return g_synced; + }, 60000); + assertTrue(g_synced); +} + +test(03_update_cloud_to_device) { + g_synced = false; + auto ledger = Particle.ledger(CLOUD_TO_DEVICE_LEDGER); + ledger.onSync([](Ledger /* ledger */) { + g_synced = true; + }); +} + +test(04_validate_cloud_to_device_sync) { + waitFor([]() { + return g_synced; + }, 60000); + auto ledger = Particle.ledger(CLOUD_TO_DEVICE_LEDGER); + ledger.onSync(nullptr); + assertTrue(g_synced); + auto d = ledger.get(); + assertTrue((d == LedgerData{ { "b", g_seed + 2 } })); +} + +test(05_sync_device_to_cloud_max_size) { + // Use the system API to avoid allocating a lot of RAM + ledger_instance* ledger = nullptr; + assertEqual(ledger_get_instance(&ledger, DEVICE_TO_CLOUD_LEDGER, nullptr /* reserved */), 0); + SCOPE_GUARD({ + ledger_release(ledger, nullptr /* reserved */); + }); + ledger_stream* stream = nullptr; + assertEqual(ledger_open(&stream, ledger, LEDGER_STREAM_MODE_WRITE, nullptr /* reserved */), 0); + NAMED_SCOPE_GUARD(closeStreamGuard, { + ledger_close(stream, 0 /* flags */, nullptr /* reserved */); + }); + // Data for this test was generated using the ledger test tool: + // https://github.com/particle-iot/device-os/tree/develop/sc-120056/user/tests/app/ledger_test + static const uint8_t cborData[] = { 0xb8, 0xd4, 0x61, 0x33, 0x78, 0x74, 0x35, 0x51, 0x35, 0x30, 0x50, 0x50, 0x65, 0x57, 0x46, 0x42, 0x73, 0x37, 0x41, 0x37, 0x4a, 0x64, 0x68, 0x59, 0x75, 0x6e, 0x36, 0x6a, 0x64, 0x39, 0x35, 0x41, 0x79, 0x59, 0x31, 0x34, 0x79, 0x69, 0x38, 0x39, 0x56, 0x53, 0x75, 0x77, 0x42, 0x35, 0x5a, 0x71, 0x78, 0x31, 0x51, 0x34, 0x75, 0x5a, 0x50, 0x43, 0x42, 0x43, 0x35, 0x69, 0x75, 0x6f, 0x4c, 0x6c, 0x72, 0x62, 0x41, 0x63, 0x44, 0x71, 0x42, 0x31, 0x62, 0x77, 0x6c, 0x63, 0x30, 0x5f, 0x69, 0x69, 0x55, 0x56, 0x65, 0x57, 0x67, 0x6c, 0x51, 0x37, 0x34, 0x77, 0x72, 0x57, 0x50, 0x65, 0x54, 0x78, 0x31, 0x6c, 0x4d, 0x33, 0x5a, 0x4e, 0x50, 0x79, 0x67, 0x33, 0x46, 0x72, 0x6e, 0x69, 0x46, 0x43, 0x44, 0x38, 0x32, 0x69, 0x36, 0x56, 0x6c, 0x63, 0x6c, 0x73, 0x62, 0x35, 0x35, 0x78, 0x51, 0x68, 0x7a, 0x73, 0x33, 0x48, 0x4b, 0x6f, 0x6c, 0x30, 0x6b, 0x63, 0x31, 0x46, 0x73, 0x51, 0x6b, 0x6e, 0x62, 0x38, 0x61, 0x74, 0x36, 0x56, 0x64, 0x61, 0x35, 0x76, 0x78, 0x47, 0x36, 0x35, 0x53, 0x68, 0x38, 0x78, 0x6d, 0x71, 0x71, 0x70, 0x34, 0x62, 0x69, 0x75, 0x66, 0x6c, 0x50, 0x47, 0x4c, 0x5f, 0x33, 0x47, 0x6d, 0x49, 0x63, 0x30, 0x4d, 0x39, 0x48, 0x74, 0x6d, 0x6b, 0x39, 0x57, 0x4a, 0x73, 0x6e, 0x30, 0x65, 0x7a, 0x50, 0x6b, 0x50, 0x45, 0x6c, 0x71, 0x59, 0x5f, 0x47, 0x49, 0x39, 0x65, 0x61, 0x58, 0x63, 0x6f, 0x75, 0x38, 0x66, 0x43, 0x4d, 0x73, 0x79, 0x57, 0x39, 0x78, 0x30, 0x5f, 0x72, 0x30, 0x74, 0x4b, 0x6a, 0x61, 0x69, 0x4b, 0x4b, 0x64, 0x70, 0x77, 0x65, 0x78, 0x70, 0x63, 0x67, 0x42, 0x74, 0x54, 0x6e, 0x51, 0x76, 0x53, 0x59, 0x64, 0x32, 0x76, 0x56, 0x38, 0x6a, 0x30, 0x72, 0x46, 0x71, 0x6f, 0x63, 0x50, 0x33, 0x70, 0x54, 0x77, 0x4c, 0x39, 0x32, 0x43, 0x77, 0x78, 0x2f, 0x71, 0x69, 0x78, 0x66, 0x53, 0x44, 0x43, 0x55, 0x52, 0x57, 0x68, 0x67, 0x53, 0x75, 0x49, 0x37, 0x4c, 0x75, 0x41, 0x53, 0x7a, 0x52, 0x50, 0x71, 0x67, 0x67, 0x50, 0x49, 0x4b, 0x33, 0x63, 0x6f, 0x4b, 0x63, 0x76, 0x31, 0x4f, 0x62, 0x43, 0x54, 0x42, 0x6a, 0x47, 0x61, 0x38, 0x78, 0x59, 0x78, 0x3b, 0x6e, 0x6a, 0x52, 0x4b, 0x43, 0x62, 0x32, 0x4a, 0x55, 0x69, 0x30, 0x78, 0x53, 0x62, 0x69, 0x6e, 0x67, 0x71, 0x7a, 0x77, 0x6f, 0x51, 0x71, 0x68, 0x77, 0x67, 0x38, 0x65, 0x73, 0x62, 0x44, 0x52, 0x5a, 0x41, 0x78, 0x54, 0x34, 0x5f, 0x6f, 0x53, 0x42, 0x71, 0x62, 0x32, 0x6b, 0x32, 0x52, 0x70, 0x67, 0x4c, 0x61, 0x32, 0x5a, 0x69, 0x77, 0x76, 0x68, 0x75, 0x77, 0x78, 0x23, 0x6c, 0x38, 0x57, 0x42, 0x45, 0x64, 0x4c, 0x76, 0x62, 0x30, 0x6b, 0x33, 0x59, 0x73, 0x66, 0x46, 0x6d, 0x61, 0x41, 0x68, 0x6a, 0x41, 0x5f, 0x57, 0x4e, 0x61, 0x51, 0x35, 0x66, 0x52, 0x48, 0x6b, 0x67, 0x73, 0x58, 0xa5, 0x62, 0x33, 0x32, 0x69, 0x46, 0x31, 0x61, 0x35, 0x4b, 0x44, 0x78, 0x64, 0x56, 0x61, 0x41, 0x63, 0x4b, 0x52, 0x49, 0x64, 0x56, 0x4b, 0x62, 0x67, 0x6f, 0x6a, 0x55, 0x46, 0x74, 0x69, 0x37, 0x68, 0x6d, 0x62, 0x79, 0x77, 0x75, 0x58, 0x4b, 0x75, 0x62, 0x42, 0x45, 0x66, 0x38, 0x47, 0x35, 0x5f, 0x46, 0x58, 0x62, 0x52, 0x56, 0x71, 0x6d, 0x45, 0x67, 0x65, 0x72, 0x30, 0x32, 0x37, 0x51, 0x67, 0x52, 0x4c, 0x6c, 0x32, 0x76, 0x6f, 0x4d, 0x74, 0x30, 0x47, 0x6f, 0x35, 0x71, 0x34, 0x37, 0x72, 0x52, 0x30, 0x75, 0x71, 0x62, 0x42, 0x63, 0x67, 0x6e, 0x4a, 0x31, 0x52, 0xa8, 0x61, 0x63, 0x60, 0x65, 0x43, 0x45, 0x75, 0x44, 0x67, 0x68, 0x61, 0x70, 0x67, 0x61, 0x72, 0x32, 0x38, 0x63, 0x62, 0x72, 0x72, 0x67, 0x5f, 0x35, 0x58, 0x71, 0x75, 0x51, 0x77, 0x63, 0x4e, 0x6a, 0x6c, 0xa1, 0x61, 0x70, 0x62, 0x4a, 0x53, 0x66, 0x39, 0x6e, 0x45, 0x73, 0x39, 0x72, 0x67, 0x6b, 0x42, 0x72, 0x65, 0x4f, 0x44, 0x45, 0x61, 0x73, 0x64, 0x4e, 0x38, 0x52, 0x73, 0x62, 0x56, 0x32, 0x68, 0x77, 0x4f, 0x65, 0x61, 0x55, 0x32, 0x67, 0x61, 0x62, 0x30, 0x50, 0x6b, 0x68, 0x52, 0x39, 0x4d, 0x51, 0x61, 0x4d, 0x56, 0x43, 0x78, 0x78, 0x66, 0x37, 0x68, 0x68, 0x76, 0x34, 0x6b, 0x78, 0x59, 0x52, 0x54, 0x58, 0x4a, 0x49, 0x68, 0x31, 0x36, 0x55, 0x50, 0x32, 0x51, 0x4f, 0x7a, 0x57, 0x42, 0x58, 0x59, 0x4c, 0x59, 0x57, 0x44, 0x4b, 0x4e, 0x68, 0x78, 0x47, 0x74, 0x61, 0x62, 0x43, 0x4e, 0x33, 0x49, 0x52, 0x31, 0x62, 0x68, 0x53, 0x63, 0x5a, 0x75, 0x55, 0x39, 0x73, 0x41, 0x6b, 0x38, 0x76, 0x38, 0x67, 0x52, 0x64, 0x65, 0x53, 0x4c, 0x6f, 0x46, 0x54, 0x52, 0x47, 0x38, 0x76, 0x64, 0x4e, 0x57, 0x6c, 0x35, 0x6f, 0x55, 0x55, 0x45, 0x48, 0x31, 0x56, 0x53, 0x48, 0x51, 0x4f, 0x63, 0x35, 0x52, 0x6d, 0x53, 0x4f, 0x62, 0x47, 0x74, 0x4d, 0x68, 0x37, 0x68, 0x49, 0x4e, 0x4e, 0x71, 0x77, 0x56, 0xa6, 0x61, 0x58, 0x68, 0x6b, 0x43, 0x78, 0x4f, 0x68, 0x33, 0x65, 0x4a, 0x61, 0x70, 0x68, 0x33, 0x61, 0x52, 0x42, 0x36, 0x36, 0x33, 0x72, 0x61, 0x65, 0x65, 0x75, 0x76, 0x6a, 0x38, 0x78, 0x64, 0x32, 0x45, 0x57, 0x73, 0x64, 0x57, 0x53, 0x4f, 0x6a, 0x67, 0x69, 0x58, 0x5a, 0x4f, 0x45, 0x45, 0x50, 0x6d, 0x54, 0x77, 0x6d, 0x30, 0x31, 0x69, 0x6a, 0x69, 0x70, 0x5a, 0x77, 0x47, 0x56, 0x62, 0x72, 0x59, 0x67, 0x61, 0x37, 0x34, 0x44, 0x36, 0x4b, 0x62, 0x78, 0x1d, 0x68, 0x50, 0x65, 0x55, 0x64, 0x45, 0x71, 0x6f, 0x6f, 0x41, 0x31, 0x32, 0x57, 0x72, 0x4e, 0x47, 0x31, 0x53, 0x4a, 0x30, 0x61, 0x30, 0x78, 0x4b, 0x4a, 0x63, 0x51, 0x72, 0x4b, 0x78, 0x35, 0x7a, 0x7a, 0x5f, 0x51, 0x52, 0x31, 0x66, 0x34, 0x48, 0x51, 0x45, 0x48, 0x30, 0x4a, 0x4b, 0x76, 0x52, 0x31, 0x31, 0x30, 0x49, 0x31, 0x59, 0x52, 0x4f, 0x54, 0x61, 0x34, 0x4a, 0x4b, 0x4d, 0x36, 0x4c, 0x57, 0x30, 0x51, 0x6c, 0x31, 0x76, 0x66, 0x37, 0x42, 0x50, 0x4a, 0x72, 0x6c, 0x68, 0x4d, 0x30, 0x5a, 0x4c, 0x48, 0x49, 0x78, 0x29, 0x7a, 0x49, 0x35, 0x6f, 0x71, 0x4b, 0x74, 0x75, 0x33, 0x70, 0x34, 0x6b, 0x79, 0x74, 0x68, 0x51, 0x74, 0x43, 0x79, 0x65, 0x46, 0x66, 0x67, 0x4f, 0x4c, 0x66, 0x68, 0x6d, 0x50, 0x39, 0x4d, 0x72, 0x6b, 0x56, 0x4b, 0x75, 0x78, 0x68, 0x42, 0x4d, 0x72, 0x78, 0x2d, 0x5f, 0x62, 0x6f, 0x75, 0x54, 0x48, 0x6c, 0x4a, 0x50, 0x6c, 0x79, 0x59, 0x5f, 0x46, 0x69, 0x54, 0x62, 0x76, 0x70, 0x63, 0x6c, 0x78, 0x34, 0x58, 0x55, 0x50, 0x34, 0x56, 0x44, 0x6a, 0x5f, 0x4e, 0x44, 0x5f, 0x5f, 0x45, 0x30, 0x62, 0x6d, 0x4c, 0x72, 0x78, 0x70, 0x54, 0x31, 0x75, 0x73, 0x7a, 0x66, 0x71, 0x6e, 0x4c, 0x41, 0x4b, 0x34, 0x37, 0x5a, 0x65, 0x44, 0x30, 0x59, 0x4f, 0x68, 0x43, 0x6a, 0x6d, 0x6e, 0x78, 0x42, 0x77, 0x61, 0x39, 0x53, 0x5a, 0x39, 0x38, 0x45, 0x54, 0x7a, 0x36, 0x4e, 0x54, 0x4c, 0x4c, 0x44, 0x44, 0x64, 0x71, 0x47, 0x4d, 0x69, 0x57, 0x69, 0x58, 0x58, 0x62, 0x45, 0x79, 0x6d, 0x6f, 0x6e, 0x70, 0x50, 0x4e, 0x4d, 0x68, 0x70, 0x69, 0x37, 0x72, 0x55, 0x67, 0x49, 0x44, 0x71, 0x57, 0x45, 0x6a, 0x33, 0x52, 0x6c, 0x4a, 0x47, 0x63, 0x47, 0x69, 0x78, 0x4b, 0x53, 0x75, 0x65, 0x4f, 0x77, 0x30, 0x62, 0x6d, 0x67, 0x59, 0x36, 0x45, 0x59, 0x77, 0x68, 0x75, 0x79, 0x61, 0x56, 0x46, 0x51, 0x78, 0x51, 0x4b, 0x4f, 0x61, 0x66, 0x30, 0x6e, 0x44, 0x4b, 0x6c, 0x4b, 0x54, 0x68, 0x48, 0x46, 0x72, 0x54, 0x6e, 0x54, 0x4c, 0x59, 0x47, 0x4a, 0x4a, 0x44, 0x4f, 0x68, 0x58, 0x7a, 0x6e, 0x51, 0x74, 0x41, 0x66, 0x33, 0x72, 0x64, 0x57, 0x62, 0x74, 0x37, 0x6f, 0x32, 0x61, 0x59, 0x6b, 0x46, 0x33, 0x35, 0x73, 0x52, 0x43, 0x6e, 0x73, 0x4b, 0x42, 0x6e, 0x79, 0x73, 0x4e, 0x4c, 0x53, 0x39, 0x4e, 0x48, 0x72, 0x65, 0x58, 0x39, 0x6d, 0x47, 0x4b, 0x4c, 0x44, 0x4d, 0x7a, 0x31, 0x5a, 0x55, 0x53, 0x50, 0x31, 0x74, 0x62, 0x6d, 0x49, 0x31, 0x65, 0x57, 0x6a, 0x43, 0x56, 0x39, 0x56, 0x4d, 0x58, 0x38, 0x52, 0x4f, 0x41, 0x6d, 0x64, 0x6f, 0xa2, 0x61, 0x67, 0x68, 0x6c, 0x5f, 0x78, 0x4e, 0x32, 0x50, 0x64, 0x53, 0x61, 0x64, 0x66, 0x53, 0x77, 0x61, 0x6c, 0x58, 0x7a, 0x78, 0x1e, 0x31, 0x65, 0x35, 0x63, 0x4d, 0x62, 0x4a, 0x76, 0x69, 0x34, 0x66, 0x68, 0x65, 0x75, 0x49, 0x53, 0x33, 0x4b, 0x7a, 0x63, 0x64, 0x33, 0x4c, 0x5f, 0x79, 0x65, 0x59, 0x6d, 0x31, 0x45, 0x78, 0x23, 0x6a, 0x44, 0x43, 0x7a, 0x74, 0x31, 0x63, 0x37, 0x79, 0x74, 0x5f, 0x6b, 0x30, 0x64, 0x69, 0x38, 0x67, 0x33, 0x41, 0x4c, 0x59, 0x62, 0x64, 0x59, 0x54, 0x44, 0x63, 0x78, 0x45, 0x42, 0x57, 0x6f, 0x75, 0x34, 0x68, 0x6d, 0x68, 0x4f, 0x4e, 0x41, 0x38, 0x6a, 0x38, 0x7a, 0x6d, 0x61, 0x41, 0x77, 0x5a, 0x78, 0x27, 0x75, 0x53, 0x6e, 0x79, 0x78, 0x4e, 0x30, 0x70, 0x78, 0x47, 0x6a, 0x49, 0x6c, 0x56, 0x30, 0x71, 0x55, 0x44, 0x55, 0x55, 0x66, 0x56, 0x6c, 0x49, 0x34, 0x66, 0x6b, 0x41, 0x4b, 0x4d, 0x46, 0x5f, 0x49, 0x6b, 0x43, 0x6b, 0x41, 0x50, 0x37, 0x6a, 0x77, 0x75, 0x70, 0x31, 0x53, 0x77, 0x69, 0x76, 0x53, 0x6e, 0x78, 0x33, 0x4b, 0x6f, 0x65, 0x63, 0x63, 0x71, 0x33, 0x33, 0x67, 0x57, 0x76, 0x37, 0x73, 0x32, 0x55, 0x31, 0x35, 0x6d, 0x4c, 0x74, 0x39, 0x69, 0x54, 0x4a, 0x47, 0x79, 0x5f, 0x5f, 0x64, 0x58, 0x45, 0x5f, 0x4f, 0x4a, 0x46, 0x46, 0x36, 0x51, 0x71, 0x69, 0x75, 0x49, 0x44, 0x69, 0x34, 0x59, 0x64, 0x7a, 0x51, 0x6d, 0x7a, 0x66, 0x33, 0x57, 0x46, 0x37, 0x4b, 0x4b, 0x78, 0x23, 0x6f, 0x42, 0x4a, 0x63, 0x67, 0x41, 0x31, 0x61, 0x39, 0x58, 0x56, 0x64, 0x52, 0x73, 0x39, 0x68, 0x52, 0x72, 0x73, 0x6d, 0x59, 0x77, 0x53, 0x79, 0x45, 0x66, 0x69, 0x4d, 0x53, 0x43, 0x38, 0x78, 0x37, 0x72, 0x62, 0x78, 0x2b, 0x65, 0x39, 0x67, 0x68, 0x45, 0x69, 0x46, 0x42, 0x69, 0x65, 0x6d, 0x45, 0x47, 0x33, 0x78, 0x73, 0x69, 0x44, 0x4e, 0x35, 0x54, 0x4b, 0x78, 0x35, 0x6a, 0x72, 0x54, 0x4f, 0x68, 0x41, 0x74, 0x4b, 0x39, 0x48, 0x54, 0x54, 0x67, 0x6c, 0x5f, 0x49, 0x57, 0x5a, 0x7a, 0x78, 0x48, 0x58, 0x59, 0x46, 0x41, 0x63, 0x42, 0x63, 0x77, 0x78, 0x6c, 0x75, 0x51, 0x6e, 0x63, 0x43, 0x34, 0x55, 0x6d, 0x57, 0x74, 0x47, 0x32, 0x63, 0x70, 0x35, 0x74, 0x75, 0x55, 0x74, 0x36, 0x57, 0x6e, 0x36, 0x71, 0x74, 0x79, 0x6c, 0x4e, 0x61, 0x68, 0x51, 0x4f, 0x67, 0x4d, 0x74, 0x78, 0x69, 0x71, 0x66, 0x45, 0x38, 0x4e, 0x71, 0x4b, 0x35, 0x52, 0x5a, 0x74, 0x4d, 0x58, 0x4f, 0x73, 0x36, 0x66, 0x61, 0x49, 0x4e, 0x54, 0x56, 0x74, 0x58, 0x4e, 0x78, 0x29, 0x30, 0x34, 0x4f, 0x65, 0x71, 0x45, 0x77, 0x74, 0x31, 0x65, 0x4a, 0x36, 0x68, 0x56, 0x47, 0x79, 0x68, 0x58, 0x55, 0x63, 0x52, 0x58, 0x4e, 0x59, 0x45, 0x6e, 0x78, 0x61, 0x39, 0x75, 0x61, 0x51, 0x35, 0x79, 0x59, 0x49, 0x45, 0x32, 0x51, 0x30, 0x49, 0x78, 0x43, 0x67, 0x65, 0x47, 0x5a, 0x49, 0x77, 0x53, 0x43, 0x67, 0x4c, 0x30, 0x34, 0x47, 0x50, 0x4a, 0x72, 0x4d, 0x73, 0x4c, 0x66, 0x6d, 0x77, 0x58, 0x52, 0x71, 0x61, 0x48, 0x55, 0x68, 0x7a, 0x64, 0x6e, 0x75, 0x4f, 0x49, 0x35, 0x41, 0x79, 0x62, 0x72, 0x64, 0x31, 0x6f, 0x65, 0x34, 0x38, 0x39, 0x55, 0x70, 0x49, 0x49, 0x78, 0x67, 0x6c, 0x4e, 0x5a, 0x5f, 0x56, 0x72, 0x46, 0x73, 0x6c, 0x53, 0x46, 0x6c, 0x6c, 0x75, 0x68, 0x6e, 0x68, 0x45, 0x49, 0x46, 0x59, 0x6d, 0x6e, 0x78, 0x5f, 0x4a, 0x70, 0x67, 0x50, 0x43, 0x47, 0x4c, 0x68, 0x36, 0x73, 0x59, 0x32, 0x41, 0x48, 0x4b, 0x64, 0x45, 0x31, 0x74, 0x6b, 0x47, 0x69, 0x37, 0x41, 0x30, 0x38, 0x6a, 0x6e, 0x39, 0x68, 0x45, 0x43, 0x6b, 0x37, 0x75, 0x31, 0x64, 0x31, 0x35, 0x6b, 0x43, 0x65, 0x74, 0x5f, 0x4e, 0x6e, 0x67, 0x66, 0x55, 0x53, 0x44, 0x37, 0x39, 0x43, 0x6d, 0x4f, 0x6c, 0x70, 0x42, 0x34, 0x6e, 0x4d, 0x67, 0x45, 0x71, 0x49, 0x51, 0x34, 0x47, 0x34, 0x63, 0x39, 0x49, 0x6b, 0x36, 0x6f, 0x35, 0x74, 0x52, 0x79, 0x54, 0x58, 0x54, 0x70, 0x31, 0x4b, 0x4c, 0x38, 0x59, 0x44, 0x6e, 0x62, 0x45, 0x44, 0x55, 0x73, 0x39, 0x4d, 0x32, 0x66, 0x75, 0x37, 0x35, 0x50, 0x4c, 0x46, 0x6f, 0x68, 0x76, 0x4f, 0x63, 0x74, 0x62, 0x67, 0x56, 0x78, 0x5e, 0x51, 0x68, 0x47, 0x53, 0x41, 0x6c, 0x61, 0x67, 0x48, 0x76, 0x78, 0x73, 0x75, 0x43, 0x4e, 0x59, 0x52, 0x6d, 0x31, 0x65, 0x61, 0x5f, 0x78, 0x32, 0x36, 0x39, 0x6b, 0x30, 0x50, 0x65, 0x43, 0x59, 0x6b, 0x51, 0x6e, 0x4a, 0x54, 0x5f, 0x55, 0x52, 0x72, 0x43, 0x74, 0x4f, 0x53, 0x47, 0x34, 0x30, 0x57, 0x31, 0x47, 0x39, 0x61, 0x36, 0x47, 0x62, 0x68, 0x53, 0x45, 0x66, 0x34, 0x79, 0x77, 0x64, 0x59, 0x58, 0x74, 0x47, 0x34, 0x6c, 0x51, 0x76, 0x70, 0x61, 0x66, 0x61, 0x64, 0x4a, 0x32, 0x52, 0x57, 0x30, 0x56, 0x57, 0x44, 0x78, 0x48, 0x39, 0x46, 0x34, 0x30, 0x6f, 0x50, 0x65, 0x69, 0x43, 0x6e, 0x47, 0x37, 0x79, 0x50, 0x79, 0x30, 0x32, 0x78, 0x4c, 0x38, 0x34, 0x35, 0x75, 0x65, 0x41, 0x61, 0x73, 0x43, 0x32, 0x5f, 0x45, 0x67, 0x76, 0x72, 0x53, 0x52, 0x4c, 0x44, 0x56, 0x70, 0x41, 0x7a, 0x30, 0x6d, 0x30, 0x4b, 0x56, 0x41, 0x5f, 0x57, 0x51, 0x48, 0x63, 0x69, 0x59, 0x55, 0x42, 0x44, 0x5a, 0x32, 0x68, 0x79, 0x68, 0x31, 0x53, 0x72, 0x39, 0x6d, 0x78, 0x76, 0x37, 0x65, 0x35, 0x6f, 0x42, 0x6a, 0x31, 0x35, 0x65, 0x32, 0x56, 0x48, 0x61, 0x68, 0x52, 0x6b, 0x37, 0x41, 0x49, 0x57, 0x63, 0x51, 0x62, 0x38, 0x47, 0x78, 0x1c, 0x6d, 0x4f, 0x44, 0x47, 0x5a, 0x6b, 0x42, 0x51, 0x33, 0x4a, 0x71, 0x61, 0x44, 0x6c, 0x66, 0x37, 0x68, 0x53, 0x4b, 0x36, 0x41, 0x6c, 0x4b, 0x75, 0x75, 0x67, 0x55, 0x66, 0x78, 0x39, 0x6b, 0x35, 0x4f, 0x78, 0x68, 0x46, 0x39, 0x75, 0x77, 0x57, 0x52, 0x6f, 0x6c, 0x30, 0x77, 0x5f, 0x6f, 0x33, 0x59, 0x48, 0x34, 0x43, 0x4d, 0x30, 0x45, 0x43, 0x7a, 0x72, 0x6f, 0x41, 0x56, 0x4a, 0x66, 0x6c, 0x69, 0x49, 0x57, 0x42, 0x35, 0x6e, 0x63, 0x70, 0x44, 0x73, 0x56, 0x73, 0x50, 0x50, 0x65, 0x30, 0x6b, 0x76, 0x4e, 0x41, 0x48, 0x4c, 0x43, 0x6d, 0x52, 0x79, 0x37, 0x6a, 0x45, 0x70, 0x75, 0x45, 0x35, 0x63, 0x45, 0x4b, 0x52, 0x78, 0x1f, 0x76, 0x49, 0x68, 0x36, 0x61, 0x47, 0x4d, 0x44, 0x4b, 0x4e, 0x4c, 0x4c, 0x43, 0x64, 0x6d, 0x6f, 0x52, 0x54, 0x42, 0x51, 0x66, 0x59, 0x50, 0x7a, 0x6e, 0x5a, 0x51, 0x72, 0x55, 0x46, 0x74, 0x70, 0x32, 0x6b, 0x76, 0x37, 0x53, 0x51, 0x56, 0x33, 0x6a, 0x4a, 0x44, 0x51, 0x78, 0x72, 0x75, 0x73, 0x78, 0x25, 0x43, 0x7a, 0x6a, 0x59, 0x55, 0x77, 0x4b, 0x76, 0x55, 0x6d, 0x59, 0x6a, 0x56, 0x51, 0x4c, 0x32, 0x53, 0x78, 0x4b, 0x6d, 0x39, 0x7a, 0x56, 0x5f, 0x32, 0x77, 0x32, 0x6e, 0x58, 0x43, 0x4a, 0x76, 0x59, 0x43, 0x6f, 0x39, 0x38, 0x67, 0x44, 0x7a, 0x71, 0x66, 0x79, 0x57, 0x32, 0x78, 0x29, 0x64, 0x4e, 0x4e, 0x67, 0x63, 0x36, 0x47, 0x58, 0x4e, 0x66, 0x61, 0x73, 0x36, 0x73, 0x44, 0x64, 0x55, 0x57, 0x44, 0x6a, 0x71, 0x6b, 0x5a, 0x5f, 0x62, 0x33, 0x63, 0x52, 0x75, 0x35, 0x66, 0x6a, 0x4d, 0x50, 0x50, 0x70, 0x32, 0x33, 0x61, 0x65, 0x71, 0x78, 0x1d, 0x4e, 0x41, 0x39, 0x70, 0x51, 0x7a, 0x79, 0x4c, 0x44, 0x32, 0x4e, 0x39, 0x75, 0x73, 0x4f, 0x78, 0x36, 0x4b, 0x4e, 0x39, 0x67, 0x56, 0x4d, 0x57, 0x45, 0x47, 0x77, 0x73, 0x69, 0x78, 0x58, 0x30, 0x79, 0x65, 0x38, 0x4b, 0x6b, 0x4b, 0x55, 0x4b, 0x47, 0x6a, 0x43, 0x79, 0x78, 0x6e, 0x6e, 0x67, 0x31, 0x54, 0x39, 0x58, 0x57, 0x38, 0x77, 0x47, 0x65, 0x6e, 0x71, 0x53, 0x6d, 0x45, 0x39, 0x33, 0x50, 0x5f, 0x7a, 0x71, 0x57, 0x6d, 0x4c, 0x4d, 0x31, 0x33, 0x56, 0x46, 0x55, 0x44, 0x5f, 0x30, 0x51, 0x4b, 0x37, 0x51, 0x6d, 0x55, 0x4f, 0x78, 0x4b, 0x63, 0x5a, 0x74, 0x77, 0x45, 0x39, 0x79, 0x4f, 0x38, 0x78, 0x30, 0x36, 0x6f, 0x45, 0x53, 0x31, 0x6e, 0x51, 0x6d, 0x4a, 0x50, 0x72, 0x7a, 0x41, 0x30, 0x6e, 0x4d, 0x5f, 0x39, 0x6d, 0x78, 0x1d, 0x46, 0x57, 0x4a, 0x73, 0x38, 0x66, 0x6c, 0x54, 0x4c, 0x76, 0x67, 0x64, 0x6e, 0x53, 0x67, 0x43, 0x69, 0x52, 0x70, 0x5a, 0x62, 0x41, 0x4b, 0x44, 0x56, 0x31, 0x36, 0x54, 0x54, 0x78, 0x1e, 0x36, 0x56, 0x46, 0x4a, 0x33, 0x62, 0x78, 0x58, 0x6b, 0x52, 0x5a, 0x69, 0x4f, 0x6b, 0x46, 0x48, 0x75, 0x37, 0x5f, 0x38, 0x65, 0x66, 0x55, 0x58, 0x7a, 0x67, 0x5f, 0x46, 0x4d, 0x33, 0x78, 0x22, 0x41, 0x50, 0x42, 0x6a, 0x76, 0x61, 0x6e, 0x70, 0x48, 0x5f, 0x50, 0x4d, 0x35, 0x4c, 0x71, 0x6f, 0x64, 0x6f, 0x4c, 0x77, 0x4a, 0x6c, 0x49, 0x31, 0x38, 0x7a, 0x37, 0x46, 0x67, 0x5f, 0x35, 0x6e, 0x38, 0x6b, 0x78, 0x52, 0x56, 0x43, 0x49, 0x6a, 0x39, 0x73, 0x6e, 0x52, 0x31, 0x30, 0x37, 0x79, 0x6d, 0x43, 0x5a, 0x53, 0x5f, 0x51, 0x35, 0x46, 0x5f, 0x6c, 0x73, 0x76, 0x53, 0x30, 0x50, 0x4f, 0x52, 0x47, 0x65, 0x78, 0x59, 0x7a, 0x38, 0x73, 0x45, 0x67, 0x58, 0x35, 0x6a, 0x67, 0x59, 0x70, 0x77, 0x6d, 0x6b, 0x43, 0x49, 0x6c, 0x6a, 0x42, 0x47, 0x6f, 0x58, 0x41, 0x61, 0x50, 0x70, 0x4b, 0x44, 0x76, 0x78, 0x69, 0x4b, 0x4a, 0x69, 0x56, 0x61, 0x5f, 0x72, 0x4b, 0x46, 0x72, 0x58, 0x39, 0x70, 0x53, 0x37, 0x36, 0x42, 0x6f, 0x78, 0x22, 0x47, 0x6c, 0x45, 0x6f, 0x32, 0x44, 0x6e, 0x48, 0x36, 0x50, 0x5f, 0x48, 0x5a, 0x65, 0x69, 0x33, 0x63, 0x6a, 0x6b, 0x50, 0x4f, 0x72, 0x7a, 0x6f, 0x67, 0x45, 0x77, 0x42, 0x30, 0x4b, 0x6c, 0x68, 0x73, 0x54, 0x78, 0x4c, 0x43, 0x4f, 0x4d, 0x30, 0x55, 0x77, 0x46, 0x44, 0x77, 0x37, 0x57, 0x49, 0x35, 0x38, 0x48, 0x34, 0x4f, 0x4e, 0x4f, 0x52, 0x6d, 0x30, 0x64, 0x79, 0x4a, 0x6e, 0x34, 0x37, 0x4c, 0x78, 0x35, 0x7a, 0x67, 0x54, 0x6b, 0x7a, 0x70, 0x76, 0x55, 0x34, 0x38, 0x74, 0x50, 0x47, 0x68, 0x4c, 0x79, 0x53, 0x46, 0x79, 0x61, 0x43, 0x56, 0x41, 0x6e, 0x76, 0x31, 0x6d, 0x76, 0x38, 0x73, 0x79, 0x4f, 0x4d, 0x59, 0x74, 0x35, 0x57, 0x51, 0x32, 0x5f, 0x6e, 0x37, 0x51, 0x48, 0x5f, 0x6b, 0x36, 0x67, 0x39, 0x32, 0x77, 0x65, 0x61, 0x6c, 0x73, 0x69, 0x63, 0x78, 0x2b, 0x72, 0x6c, 0x78, 0x4c, 0x32, 0x67, 0x5a, 0x42, 0x78, 0x6e, 0x32, 0x74, 0x5a, 0x31, 0x71, 0x36, 0x53, 0x30, 0x46, 0x4a, 0x32, 0x43, 0x39, 0x41, 0x6d, 0x69, 0x38, 0x74, 0x35, 0x46, 0x69, 0x67, 0x36, 0x67, 0x33, 0x6d, 0x46, 0x50, 0x67, 0x46, 0x64, 0x6a, 0x77, 0x67, 0x42, 0x79, 0x70, 0x6f, 0x78, 0x52, 0x59, 0x78, 0x59, 0x58, 0x76, 0x54, 0x49, 0x74, 0x45, 0x6f, 0x56, 0x6e, 0x59, 0x59, 0x55, 0x58, 0x45, 0x69, 0x39, 0x6c, 0x49, 0x61, 0x64, 0x57, 0x73, 0x73, 0x6f, 0x31, 0x4e, 0x36, 0x59, 0x65, 0x64, 0x41, 0x52, 0x57, 0x72, 0x75, 0x74, 0x56, 0x77, 0x6c, 0x5a, 0x5a, 0x70, 0x78, 0x69, 0x52, 0x36, 0x38, 0x72, 0x79, 0x4b, 0x33, 0x47, 0x47, 0x51, 0x79, 0x53, 0x75, 0x52, 0x6c, 0x6c, 0x39, 0x38, 0x6f, 0x30, 0x30, 0x66, 0x37, 0x69, 0x76, 0x50, 0x62, 0x42, 0x70, 0x6a, 0x4d, 0x6e, 0x65, 0x78, 0x6f, 0x7a, 0x67, 0x35, 0x4a, 0x6d, 0x66, 0x48, 0x48, 0x74, 0x77, 0x67, 0x61, 0x67, 0x71, 0x6c, 0x69, 0x42, 0x63, 0x78, 0x4f, 0x42, 0x70, 0x4c, 0x59, 0x31, 0x34, 0x72, 0x74, 0x6e, 0x67, 0x5a, 0x71, 0x78, 0x5f, 0x7a, 0x47, 0x33, 0x52, 0x37, 0x58, 0x70, 0x61, 0x31, 0x76, 0x41, 0x56, 0x62, 0x67, 0x4b, 0x66, 0x79, 0x34, 0x51, 0x37, 0x59, 0x54, 0x6b, 0x70, 0x62, 0x4e, 0x36, 0x42, 0x43, 0x50, 0x68, 0x49, 0x70, 0x51, 0x36, 0x30, 0x4e, 0x42, 0x34, 0x6e, 0x51, 0x4d, 0x31, 0x4a, 0x39, 0x53, 0x33, 0x68, 0x38, 0x5f, 0x6f, 0x56, 0x71, 0x55, 0x4c, 0x72, 0x73, 0x36, 0x59, 0x4d, 0x61, 0x52, 0x5a, 0x69, 0x79, 0x68, 0x6d, 0x78, 0x47, 0x76, 0x33, 0x6c, 0x43, 0x79, 0x78, 0x2a, 0x39, 0x6c, 0x34, 0x36, 0x55, 0x59, 0x6a, 0x61, 0x75, 0x67, 0x6e, 0x42, 0x51, 0x56, 0x42, 0x55, 0x42, 0x68, 0x43, 0x37, 0x4d, 0x43, 0x78, 0x6b, 0x58, 0x7a, 0x61, 0x53, 0x43, 0x53, 0x59, 0x38, 0x65, 0x53, 0x38, 0x47, 0x6a, 0x54, 0x4b, 0x4a, 0x68, 0x6e, 0x61, 0x6d, 0x78, 0x21, 0x41, 0x46, 0x71, 0x33, 0x4a, 0x62, 0x68, 0x38, 0x46, 0x5f, 0x4c, 0x58, 0x5f, 0x5f, 0x72, 0x5f, 0x72, 0x62, 0x6b, 0x54, 0x63, 0x31, 0x39, 0x59, 0x6f, 0x43, 0x6d, 0x50, 0x33, 0x32, 0x6e, 0x73, 0x42, 0x78, 0x22, 0x5f, 0x66, 0x69, 0x5f, 0x37, 0x33, 0x36, 0x31, 0x38, 0x44, 0x42, 0x58, 0x30, 0x56, 0x39, 0x73, 0x61, 0x4d, 0x4b, 0x49, 0x4f, 0x45, 0x47, 0x6a, 0x56, 0x31, 0x67, 0x45, 0x49, 0x6f, 0x38, 0x78, 0x35, 0x44, 0x78, 0x52, 0x4e, 0x4a, 0x32, 0x38, 0x4a, 0x52, 0x34, 0x51, 0x6e, 0x48, 0x54, 0x78, 0x69, 0x47, 0x31, 0x61, 0x64, 0x4f, 0x57, 0x70, 0x37, 0x62, 0x69, 0x67, 0x6c, 0x38, 0x4d, 0x35, 0x57, 0x65, 0x6d, 0x6b, 0x42, 0x57, 0x54, 0x6d, 0x39, 0x66, 0x46, 0x79, 0x72, 0x41, 0x56, 0x6a, 0x57, 0x54, 0x42, 0x32, 0x66, 0x50, 0x55, 0x79, 0x34, 0x48, 0x49, 0x4d, 0x57, 0x6d, 0x68, 0x66, 0x31, 0x46, 0x6e, 0x6e, 0x46, 0x4e, 0x47, 0x56, 0x71, 0x78, 0x49, 0x55, 0x71, 0x6e, 0x34, 0x4a, 0x48, 0x64, 0x4e, 0x63, 0x6e, 0x48, 0x6d, 0x6e, 0x67, 0x45, 0x58, 0x4a, 0x4f, 0x78, 0x38, 0x48, 0x32, 0x67, 0x59, 0x45, 0x71, 0x66, 0x57, 0x48, 0x72, 0x7a, 0x56, 0x39, 0x51, 0x7a, 0x36, 0x34, 0x6e, 0x4f, 0x6a, 0x54, 0x5f, 0x72, 0x78, 0x21, 0x68, 0x48, 0x4a, 0x6e, 0x42, 0x62, 0x6b, 0x37, 0x68, 0x53, 0x5f, 0x66, 0x52, 0x34, 0x54, 0x48, 0x55, 0x44, 0x70, 0x73, 0x64, 0x78, 0x57, 0x30, 0x53, 0x43, 0x56, 0x79, 0x67, 0x47, 0x59, 0x42, 0x4c, 0xa3, 0x61, 0x67, 0x6c, 0x65, 0x54, 0x4f, 0x34, 0x46, 0x69, 0x59, 0x65, 0x4b, 0x74, 0x43, 0x70, 0x63, 0x48, 0x65, 0x63, 0x67, 0x33, 0x36, 0x77, 0x64, 0x50, 0x31, 0x45, 0x62, 0x6b, 0x58, 0x64, 0x35, 0x54, 0x43, 0x76, 0x78, 0x1f, 0x58, 0x55, 0x79, 0x71, 0x44, 0x37, 0x4a, 0x54, 0x64, 0x58, 0x32, 0x55, 0x36, 0x55, 0x73, 0x70, 0x67, 0x71, 0x65, 0x59, 0x62, 0x42, 0x75, 0x37, 0x6d, 0x7a, 0x37, 0x68, 0x61, 0x79, 0x52, 0x78, 0x26, 0x46, 0x67, 0x66, 0x73, 0x65, 0x6d, 0x50, 0x41, 0x31, 0x7a, 0x47, 0x41, 0x6b, 0x4f, 0x78, 0x4c, 0x32, 0x4e, 0x77, 0x45, 0x6a, 0x76, 0x6f, 0x68, 0x57, 0x75, 0x58, 0x4c, 0x41, 0x69, 0x31, 0x34, 0x36, 0x6c, 0x74, 0x55, 0x41, 0x50, 0x62, 0x6b, 0x68, 0x78, 0x36, 0x68, 0x4c, 0x68, 0x67, 0x4e, 0x5f, 0x32, 0x4a, 0x77, 0x34, 0x75, 0x47, 0x42, 0x69, 0x66, 0x6f, 0x61, 0x73, 0x31, 0x44, 0x56, 0x43, 0x77, 0x6d, 0x78, 0x4f, 0x59, 0x65, 0x75, 0x43, 0x6a, 0x74, 0x77, 0x76, 0x47, 0x6e, 0x65, 0x42, 0x64, 0x61, 0x57, 0x5f, 0x34, 0x67, 0x32, 0x6d, 0x48, 0x55, 0x35, 0x39, 0x6d, 0x6d, 0x34, 0x39, 0x69, 0x51, 0x6c, 0x66, 0x58, 0x62, 0x38, 0x57, 0x39, 0x4b, 0x78, 0x63, 0x54, 0x48, 0x4a, 0x49, 0x62, 0x67, 0x37, 0x54, 0x47, 0x5a, 0x31, 0x44, 0x72, 0x42, 0x6d, 0x4e, 0x78, 0x6d, 0x70, 0x63, 0x64, 0x4b, 0x42, 0x69, 0x6b, 0x51, 0x47, 0x76, 0x33, 0x50, 0x66, 0x45, 0x61, 0x6f, 0x58, 0x5a, 0x58, 0x6b, 0x30, 0x39, 0x6c, 0x43, 0x4e, 0x42, 0x52, 0x56, 0x59, 0x43, 0x49, 0x56, 0x4a, 0x61, 0x57, 0x30, 0x4b, 0x45, 0x4d, 0x74, 0x62, 0x30, 0x45, 0x57, 0x7a, 0x55, 0x5a, 0x63, 0x52, 0x56, 0x78, 0x41, 0x73, 0x46, 0x72, 0x78, 0x34, 0x79, 0x54, 0x6b, 0x57, 0x45, 0x44, 0x33, 0x58, 0x7a, 0x53, 0x65, 0x38, 0x62, 0x46, 0x5f, 0x67, 0x5a, 0x68, 0x46, 0x6f, 0x71, 0x51, 0x5a, 0x4a, 0x76, 0x30, 0x44, 0x76, 0x72, 0x56, 0x52, 0x6a, 0x55, 0x6e, 0x6e, 0x38, 0x5f, 0x62, 0x56, 0x79, 0x66, 0x7a, 0x44, 0x76, 0x62, 0x37, 0x61, 0x76, 0x4c, 0x52, 0x58, 0x55, 0x59, 0x44, 0x49, 0x34, 0x58, 0x68, 0x72, 0x4c, 0x4c, 0x41, 0x5f, 0x48, 0x75, 0x68, 0x5a, 0x58, 0x65, 0x73, 0x78, 0x27, 0x79, 0x34, 0x71, 0x72, 0x6e, 0x5f, 0x36, 0x6d, 0x34, 0x38, 0x63, 0x66, 0x66, 0x68, 0x56, 0x48, 0x63, 0x6d, 0x77, 0x76, 0x55, 0x4c, 0x65, 0x59, 0x43, 0x6a, 0x38, 0x45, 0x35, 0x41, 0x43, 0x63, 0x78, 0x65, 0x51, 0x34, 0x39, 0x49, 0x54, 0x78, 0x4a, 0x4e, 0x46, 0x6f, 0x75, 0x49, 0x4e, 0x6b, 0x4a, 0x58, 0x6a, 0x30, 0x61, 0x73, 0x71, 0x69, 0x54, 0x6e, 0x6f, 0x5f, 0x33, 0x67, 0x30, 0x31, 0x47, 0x36, 0x67, 0x6f, 0x66, 0x31, 0x30, 0x6c, 0x37, 0x4e, 0x6c, 0x39, 0x34, 0x33, 0x59, 0x37, 0x6b, 0x66, 0x38, 0x61, 0x48, 0x4e, 0x67, 0x4d, 0x49, 0x35, 0x4e, 0x4e, 0x38, 0x69, 0x32, 0x52, 0x73, 0x50, 0x74, 0x72, 0x69, 0x67, 0x65, 0x70, 0x70, 0x38, 0x71, 0x55, 0x77, 0x62, 0x79, 0x52, 0x76, 0x75, 0x6c, 0x6a, 0x53, 0x79, 0x5f, 0x77, 0x33, 0x4a, 0x74, 0x79, 0x57, 0x56, 0x78, 0x3c, 0x74, 0x6c, 0x4b, 0x59, 0x50, 0x6b, 0x71, 0x6e, 0x75, 0x47, 0x52, 0x57, 0x5a, 0x4f, 0x47, 0x79, 0x4d, 0x4b, 0x79, 0x56, 0x6c, 0x61, 0x7a, 0x67, 0x5a, 0x6e, 0x34, 0x36, 0x61, 0x50, 0x74, 0x6e, 0x4b, 0x4b, 0x59, 0x77, 0x54, 0x35, 0x71, 0x62, 0x58, 0x42, 0x46, 0x63, 0x76, 0x52, 0x63, 0x55, 0x46, 0x39, 0x68, 0x56, 0x4f, 0x31, 0x30, 0x59, 0x64, 0x71, 0x51, 0x58, 0x78, 0x22, 0x54, 0x6a, 0x46, 0x57, 0x6f, 0x36, 0x33, 0x54, 0x6e, 0x42, 0x38, 0x37, 0x44, 0x4d, 0x33, 0x71, 0x57, 0x75, 0x65, 0x79, 0x47, 0x47, 0x47, 0x32, 0x31, 0x46, 0x63, 0x55, 0x30, 0x68, 0x6a, 0x41, 0x38, 0x54, 0x78, 0x42, 0x4e, 0x67, 0x49, 0x4a, 0x6a, 0x69, 0x47, 0x6e, 0x76, 0x77, 0x69, 0x7a, 0x66, 0x31, 0x71, 0x4e, 0x77, 0x43, 0x38, 0x63, 0x74, 0x61, 0x56, 0x6b, 0x71, 0x4a, 0x78, 0x61, 0x39, 0x63, 0x53, 0x52, 0x35, 0x4b, 0x59, 0x4b, 0x68, 0x31, 0x44, 0x55, 0x50, 0x41, 0x68, 0x6a, 0x66, 0x48, 0x48, 0x52, 0x33, 0x62, 0x58, 0x51, 0x52, 0x52, 0x63, 0x36, 0x66, 0x5a, 0x31, 0x49, 0x78, 0x34, 0x79, 0x59, 0x6e, 0x61, 0x6f, 0x71, 0x44, 0x78, 0x77, 0x59, 0x30, 0x57, 0x45, 0x54, 0x77, 0x5f, 0x5a, 0x41, 0x63, 0x55, 0x78, 0x23, 0x75, 0x5f, 0x4b, 0x5a, 0x79, 0x78, 0x64, 0x78, 0x36, 0x79, 0x78, 0x6a, 0x4e, 0x59, 0x64, 0x48, 0x35, 0x63, 0x34, 0x5a, 0x46, 0x31, 0x42, 0x6d, 0x72, 0x4e, 0x4d, 0x68, 0x6e, 0x72, 0x70, 0x46, 0x7a, 0x48, 0x4c, 0x67, 0x70, 0x47, 0x54, 0x44, 0x41, 0x7a, 0x63, 0x78, 0x33, 0x56, 0x6e, 0x52, 0x50, 0x79, 0x58, 0x49, 0x44, 0x35, 0x77, 0x72, 0x6c, 0x62, 0x57, 0x58, 0x44, 0x72, 0x38, 0x4f, 0x70, 0x71, 0x57, 0x47, 0x42, 0x70, 0x38, 0x74, 0x52, 0x36, 0x62, 0x51, 0x64, 0x6f, 0x36, 0x74, 0x4a, 0x41, 0x32, 0x30, 0x52, 0x57, 0x50, 0x55, 0x69, 0x53, 0x48, 0x39, 0x32, 0x6f, 0x45, 0x4e, 0x78, 0x1f, 0x72, 0x6e, 0x4d, 0x45, 0x36, 0x34, 0x6e, 0x44, 0x58, 0x6d, 0x4e, 0x6c, 0x4e, 0x78, 0x5a, 0x38, 0x55, 0x62, 0x43, 0x6a, 0x65, 0x52, 0x6e, 0x58, 0x4c, 0x79, 0x74, 0x69, 0x42, 0x56, 0x52, 0x78, 0x25, 0x67, 0x72, 0x4b, 0x4f, 0x49, 0x34, 0x52, 0x56, 0x62, 0x4f, 0x66, 0x70, 0x44, 0x64, 0x67, 0x6f, 0x4d, 0x69, 0x6e, 0x76, 0x57, 0x75, 0x66, 0x33, 0x74, 0x63, 0x5a, 0x68, 0x57, 0x61, 0x4f, 0x32, 0x49, 0x59, 0x4e, 0x6c, 0x32, 0x67, 0x7a, 0x79, 0x51, 0x61, 0x78, 0x51, 0x68, 0x78, 0x2b, 0x6a, 0x52, 0x4d, 0x37, 0x67, 0x51, 0x4d, 0x6d, 0x61, 0x79, 0x62, 0x42, 0x6e, 0x55, 0x46, 0x34, 0x75, 0x58, 0x7a, 0x39, 0x6f, 0x67, 0x74, 0x56, 0x61, 0x63, 0x69, 0x79, 0x71, 0x46, 0x63, 0x6e, 0x4a, 0x41, 0x62, 0x45, 0x61, 0x41, 0x63, 0x63, 0x42, 0x62, 0x78, 0x68, 0x44, 0x54, 0x52, 0x4a, 0x68, 0x35, 0x6e, 0x33, 0x78, 0x37, 0x58, 0x4d, 0x71, 0x6a, 0x31, 0x69, 0x4d, 0x36, 0x48, 0x58, 0x34, 0x70, 0x45, 0x4d, 0x6a, 0x59, 0x68, 0x51, 0x67, 0x42, 0x4d, 0x79, 0x76, 0x52, 0x41, 0x70, 0x38, 0x38, 0x5f, 0x49, 0x50, 0x6b, 0x6c, 0x55, 0x33, 0x36, 0x7a, 0x74, 0x77, 0x35, 0x6f, 0x68, 0x62, 0x45, 0x74, 0x57, 0x31, 0x75, 0x6c, 0x54, 0x79, 0x69, 0x62, 0x74, 0x42, 0x75, 0x5f, 0x38, 0x47, 0x4c, 0x4c, 0x38, 0x4b, 0x4a, 0x4f, 0x59, 0x64, 0x34, 0x6d, 0x6d, 0x4b, 0x42, 0x6e, 0x49, 0x71, 0x71, 0x57, 0x78, 0x1b, 0x71, 0x46, 0x51, 0x38, 0x32, 0x64, 0x42, 0x46, 0x54, 0x79, 0x79, 0x57, 0x5a, 0x77, 0x77, 0x4a, 0x57, 0x4c, 0x77, 0x58, 0x31, 0x4e, 0x49, 0x61, 0x6c, 0x37, 0x62, 0x78, 0x35, 0x76, 0x71, 0x33, 0x59, 0x59, 0x4a, 0x64, 0x61, 0x6b, 0x70, 0x6b, 0x70, 0x4a, 0x41, 0x4e, 0x76, 0x61, 0x45, 0x67, 0x74, 0x61, 0x35, 0x47, 0x61, 0x51, 0x57, 0x32, 0x65, 0x43, 0x49, 0x53, 0x7a, 0x72, 0x53, 0x41, 0x34, 0x44, 0x71, 0x4f, 0x69, 0x50, 0x53, 0x72, 0x38, 0x6a, 0x61, 0x55, 0x42, 0x4e, 0x6e, 0x65, 0x61, 0x41, 0x78, 0x41, 0x39, 0x48, 0x70, 0x54, 0x74, 0x51, 0x6e, 0x4d, 0x30, 0x37, 0x34, 0x7a, 0x46, 0x31, 0x4c, 0x68, 0x4f, 0x50, 0x51, 0x76, 0x33, 0x4d, 0x72, 0x34, 0x33, 0x6a, 0x75, 0x64, 0x5a, 0x4b, 0x71, 0x42, 0x70, 0x6d, 0x47, 0x71, 0x67, 0x74, 0x42, 0x64, 0x53, 0x4e, 0x39, 0x62, 0x71, 0x38, 0x31, 0x55, 0x55, 0x46, 0x4a, 0x30, 0x5a, 0x70, 0x49, 0x71, 0x72, 0x6c, 0x4a, 0x75, 0x50, 0x74, 0x78, 0x5a, 0x67, 0x69, 0x45, 0x54, 0x34, 0x6d, 0x61, 0x5a, 0x38, 0x63, 0x49, 0x78, 0x6c, 0x55, 0x77, 0x77, 0x61, 0x66, 0x48, 0x69, 0x6c, 0x69, 0x4e, 0x76, 0x4e, 0x57, 0x4c, 0x5f, 0x4b, 0x32, 0x66, 0x5f, 0x56, 0x34, 0x63, 0x71, 0x59, 0x51, 0x49, 0x75, 0x6a, 0x74, 0x65, 0x69, 0x4f, 0x63, 0x66, 0x47, 0x4b, 0x6f, 0x34, 0x75, 0x76, 0x45, 0x42, 0x52, 0x30, 0x67, 0x61, 0x30, 0x62, 0x6b, 0x6c, 0x71, 0x69, 0x57, 0x5f, 0x74, 0x55, 0x4f, 0x36, 0x76, 0x6c, 0x47, 0x4f, 0x7a, 0x4f, 0x70, 0x48, 0x78, 0x41, 0x33, 0x61, 0x78, 0x5f, 0x71, 0x45, 0x51, 0x30, 0x69, 0x38, 0x4d, 0x79, 0x56, 0x6b, 0x73, 0x45, 0x67, 0x75, 0x45, 0x70, 0x49, 0x57, 0x56, 0x79, 0x34, 0x65, 0x58, 0x35, 0x65, 0x33, 0x57, 0x45, 0x67, 0x32, 0x41, 0x49, 0x45, 0x68, 0x38, 0x4f, 0x78, 0x2d, 0x4f, 0x74, 0x41, 0x31, 0x56, 0x38, 0x76, 0x78, 0x4e, 0x57, 0x6c, 0x64, 0x55, 0x5f, 0x4e, 0x6f, 0x38, 0x73, 0x32, 0x56, 0x47, 0x56, 0x47, 0x65, 0x7a, 0x79, 0x59, 0x30, 0x6f, 0x69, 0x69, 0x74, 0x38, 0x67, 0x64, 0x6c, 0x54, 0x39, 0x74, 0x69, 0x6f, 0x6b, 0x45, 0x49, 0x44, 0xa4, 0x61, 0x37, 0x69, 0x35, 0x32, 0x78, 0x5a, 0x55, 0x62, 0x6f, 0x35, 0x79, 0x61, 0x6d, 0x63, 0x6a, 0x34, 0x6d, 0x62, 0x66, 0x6d, 0x71, 0x51, 0x6a, 0x78, 0x41, 0x65, 0x49, 0x47, 0x6b, 0x47, 0x4a, 0x62, 0x6e, 0x30, 0x6d, 0x69, 0x72, 0x6c, 0x63, 0x6c, 0x51, 0x74, 0x64, 0x70, 0x35, 0x6c, 0x69, 0x77, 0x6f, 0x4d, 0x58, 0x79, 0x56, 0x66, 0x34, 0x79, 0x5f, 0x4d, 0x5a, 0x35, 0x43, 0x71, 0x4b, 0x34, 0x32, 0x4c, 0x53, 0x31, 0x54, 0x63, 0x57, 0xa3, 0x61, 0x30, 0x60, 0x66, 0x73, 0x57, 0x43, 0x58, 0x73, 0x61, 0xa1, 0x61, 0x39, 0x68, 0x6b, 0x66, 0x38, 0x69, 0x50, 0x4f, 0x59, 0x63, 0x62, 0x6b, 0x57, 0x69, 0x65, 0x4c, 0x6f, 0x50, 0x63, 0x6b, 0x4b, 0x74, 0x36, 0x6b, 0x6d, 0x53, 0x76, 0x7a, 0x45, 0x5a, 0x51, 0x44, 0x79, 0x38, 0x47, 0xa3, 0x61, 0x55, 0x65, 0x62, 0x4c, 0x78, 0x77, 0x38, 0x63, 0x67, 0x6a, 0x53, 0x64, 0x64, 0x65, 0x45, 0x4e, 0x62, 0x43, 0x72, 0x65, 0x4b, 0x54, 0x7a, 0x6b, 0x55, 0x63, 0x35, 0x6a, 0x34, 0x78, 0x1b, 0x36, 0x4f, 0x35, 0x6e, 0x6e, 0x6e, 0x48, 0x51, 0x34, 0x54, 0x5f, 0x5f, 0x72, 0x62, 0x71, 0x5f, 0x43, 0x48, 0x43, 0x65, 0x6b, 0x71, 0x4b, 0x44, 0x46, 0x35, 0x34, 0x78, 0x18, 0x30, 0x4b, 0x32, 0x32, 0x52, 0x75, 0x6f, 0x62, 0x38, 0x33, 0x61, 0x56, 0x6d, 0x4e, 0x77, 0x53, 0x6d, 0x41, 0x59, 0x46, 0x41, 0x76, 0x55, 0x66, 0xa4, 0x61, 0x38, 0x60, 0x65, 0x49, 0x38, 0x34, 0x5f, 0x51, 0x6a, 0x41, 0x4a, 0x74, 0x69, 0x4a, 0x47, 0x47, 0x73, 0x76, 0x43, 0x63, 0x4b, 0x61, 0x71, 0x6a, 0x37, 0x43, 0x52, 0x35, 0x53, 0x74, 0x39, 0x36, 0x33, 0x62, 0x63, 0x51, 0x32, 0x74, 0x62, 0x66, 0x30, 0x6e, 0x31, 0x57, 0x32, 0x79, 0x4f, 0x58, 0x68, 0x62, 0x56, 0x57, 0x44, 0x47, 0x53, 0x7a, 0x78, 0x2a, 0x52, 0x38, 0x58, 0x62, 0x6a, 0x6a, 0x64, 0x4e, 0x37, 0x63, 0x4a, 0x56, 0x6e, 0x38, 0x73, 0x59, 0x6e, 0x4f, 0x34, 0x6c, 0x72, 0x52, 0x36, 0x48, 0x30, 0x56, 0x62, 0x32, 0x6c, 0x6d, 0x36, 0x30, 0x73, 0x37, 0x53, 0x53, 0x38, 0x47, 0x46, 0x75, 0x50, 0x78, 0x64, 0x73, 0x77, 0x36, 0x6f, 0xa7, 0x61, 0x33, 0x72, 0x69, 0x6e, 0x53, 0x58, 0x65, 0x56, 0x6d, 0x4a, 0x33, 0x5f, 0x49, 0x76, 0x73, 0x46, 0x69, 0x4e, 0x48, 0x4b, 0x63, 0x33, 0x44, 0x6c, 0x64, 0x69, 0x68, 0x49, 0x32, 0x61, 0x72, 0x6f, 0x51, 0x38, 0x77, 0x59, 0x33, 0x7a, 0x47, 0x34, 0x6d, 0x55, 0x45, 0x53, 0x5f, 0x41, 0x47, 0x63, 0x4d, 0x64, 0x5f, 0x62, 0x55, 0x77, 0x63, 0x43, 0x4b, 0x4f, 0x66, 0x62, 0x72, 0x56, 0x70, 0x31, 0x4c, 0x65, 0x59, 0x35, 0x6b, 0x4d, 0x6c, 0x6d, 0x52, 0x6b, 0x74, 0x48, 0x64, 0x42, 0x56, 0x65, 0x64, 0x71, 0x66, 0x68, 0x5a, 0x63, 0x57, 0x72, 0x31, 0x65, 0x34, 0x4a, 0x72, 0x38, 0x34, 0x6c, 0x74, 0x76, 0x4e, 0x53, 0x42, 0x36, 0x39, 0x6c, 0x7a, 0x39, 0x4c, 0x49, 0x76, 0x75, 0x66, 0x39, 0x63, 0x59, 0x33, 0x56, 0x36, 0x78, 0x53, 0x69, 0x36, 0x68, 0x31, 0x69, 0x58, 0x78, 0x4b, 0x5f, 0x37, 0x4d, 0x62, 0x78, 0x24, 0x4a, 0x49, 0x66, 0x59, 0x59, 0x66, 0x68, 0x5a, 0x68, 0x45, 0x68, 0x4a, 0x67, 0x6a, 0x57, 0x61, 0x35, 0x6c, 0x4b, 0x34, 0x74, 0x70, 0x4f, 0x31, 0x31, 0x47, 0x5a, 0x4f, 0x31, 0x57, 0x6a, 0x72, 0x65, 0x67, 0x70, 0x75, 0xa5, 0x61, 0x49, 0x66, 0x75, 0x73, 0x31, 0x72, 0x37, 0x77, 0x65, 0x42, 0x56, 0x59, 0x44, 0x54, 0x6b, 0x71, 0x6b, 0x69, 0x5f, 0x5f, 0x6f, 0x61, 0x46, 0x46, 0x34, 0x67, 0x67, 0x43, 0x4b, 0x30, 0x68, 0x5f, 0x64, 0x63, 0x67, 0x50, 0x65, 0x52, 0x30, 0x55, 0x49, 0x68, 0x64, 0x63, 0x50, 0x4f, 0x50, 0xa1, 0x61, 0x34, 0x6b, 0x38, 0x57, 0x39, 0x78, 0x6d, 0x44, 0x5f, 0x4f, 0x50, 0x6d, 0x4b, 0x63, 0x77, 0x43, 0x66, 0x65, 0x4a, 0x5f, 0x4d, 0x6b, 0x36, 0x68, 0x4e, 0x5f, 0x46, 0x35, 0x45, 0x4c, 0x41, 0x49, 0xa2, 0x61, 0x32, 0x6c, 0x38, 0x32, 0x76, 0x4a, 0x5f, 0x4d, 0x6d, 0x69, 0x46, 0x4d, 0x75, 0x6a, 0x64, 0x68, 0x73, 0x66, 0x75, 0x67, 0x4f, 0x6c, 0x35, 0x6e, 0x79, 0x6c, 0x44, 0x72, 0x77, 0x38, 0x45, 0x52, 0x67, 0x50, 0x56, 0x68, 0x45, 0x68, 0x38, 0x70, 0x6b, 0x53, 0x4a, 0x34, 0x37, 0x43, 0x78, 0x60, 0x57, 0x64, 0x58, 0x75, 0x7a, 0x75, 0x55, 0x57, 0x6c, 0x6f, 0x46, 0x78, 0x4b, 0x37, 0x33, 0x56, 0x32, 0x54, 0x67, 0x7a, 0x42, 0x69, 0x34, 0x52, 0x68, 0x53, 0x68, 0x77, 0x6d, 0x5a, 0x38, 0x42, 0x6a, 0x7a, 0x75, 0x5f, 0x51, 0x50, 0x78, 0x4b, 0x69, 0x66, 0x32, 0x45, 0x30, 0x31, 0x55, 0x67, 0x63, 0x37, 0x39, 0x59, 0x30, 0x48, 0x71, 0x6f, 0x79, 0x55, 0x33, 0x50, 0x66, 0x46, 0x41, 0x61, 0x41, 0x68, 0x76, 0x48, 0x4d, 0x37, 0x77, 0x46, 0x55, 0x49, 0x73, 0x30, 0x50, 0x69, 0x30, 0x55, 0x54, 0x34, 0x62, 0x44, 0x67, 0x72, 0x6f, 0x6e, 0x4f, 0x46, 0x47, 0x70, 0x73, 0x39, 0x72, 0x5f, 0x78, 0x28, 0x5a, 0x7a, 0x5a, 0x69, 0x32, 0x44, 0x41, 0x65, 0x55, 0x4b, 0x42, 0x48, 0x64, 0x70, 0x78, 0x36, 0x6a, 0x38, 0x69, 0x45, 0x6e, 0x4e, 0x45, 0x33, 0x31, 0x6a, 0x52, 0x73, 0x31, 0x6d, 0x32, 0x31, 0x6c, 0x38, 0x64, 0x43, 0x64, 0x49, 0x30, 0x35, 0x78, 0x32, 0x69, 0x56, 0x68, 0x54, 0x65, 0x55, 0x47, 0x45, 0x68, 0x7a, 0x4b, 0x59, 0x45, 0x48, 0x4e, 0x36, 0x6b, 0x6c, 0x45, 0x67, 0x70, 0x6e, 0x51, 0x6d, 0x6e, 0x72, 0x66, 0x6e, 0x46, 0x78, 0x33, 0x57, 0x42, 0x4e, 0x69, 0x37, 0x74, 0x50, 0x4e, 0x46, 0x64, 0x4e, 0x77, 0x69, 0x6c, 0x54, 0x4f, 0x4d, 0x43, 0x59, 0x6a, 0x66, 0x4b, 0x4e, 0x68, 0x37, 0x46, 0x33, 0x77, 0x38, 0x51, 0xa4, 0x61, 0x4c, 0x64, 0x35, 0x62, 0x51, 0x44, 0x66, 0x42, 0x46, 0x41, 0x36, 0x55, 0x71, 0x65, 0x45, 0x59, 0x7a, 0x57, 0x38, 0x63, 0x5f, 0x75, 0x52, 0x63, 0x45, 0x52, 0x46, 0x64, 0x6b, 0x51, 0x61, 0x74, 0x64, 0x37, 0x67, 0x7a, 0x64, 0x78, 0x1c, 0x78, 0x4f, 0x50, 0x4f, 0x4f, 0x71, 0x41, 0x45, 0x4e, 0x66, 0x77, 0x68, 0x30, 0x50, 0x69, 0x61, 0x67, 0x55, 0x77, 0x77, 0x67, 0x72, 0x4b, 0x79, 0x76, 0x4c, 0x67, 0x55, 0xa5, 0x61, 0x46, 0x63, 0x68, 0x36, 0x32, 0x63, 0x67, 0x6a, 0x58, 0x6e, 0x54, 0x54, 0x4d, 0x50, 0x72, 0x37, 0x44, 0x4b, 0x5f, 0x35, 0x70, 0x33, 0x39, 0x57, 0x63, 0x4c, 0x4b, 0x49, 0x66, 0x64, 0x4c, 0x64, 0x74, 0x44, 0x59, 0x65, 0x54, 0x55, 0x49, 0x63, 0x48, 0x6d, 0x74, 0x6b, 0x41, 0x72, 0x6d, 0x52, 0x79, 0x49, 0x4d, 0x45, 0x6d, 0x31, 0x52, 0x62, 0x35, 0x52, 0x6b, 0x78, 0x62, 0x34, 0x64, 0x54, 0x45, 0x46, 0x73, 0x68, 0x61, 0x49, 0x62, 0x57, 0x69, 0x78, 0x2e, 0x6d, 0x6a, 0x48, 0x32, 0x5a, 0x76, 0x51, 0x39, 0x53, 0x41, 0x57, 0x46, 0x57, 0x6e, 0x41, 0x6e, 0x64, 0x34, 0x6a, 0x38, 0x37, 0x75, 0x42, 0x72, 0x4f, 0x5a, 0x78, 0x52, 0x76, 0x58, 0x7a, 0x6b, 0x37, 0x6a, 0x63, 0x5a, 0x45, 0x31, 0x4b, 0x32, 0x7a, 0x6f, 0x4f, 0x63, 0x79, 0x4f, 0x63, 0x78, 0x65, 0x31, 0x78, 0x1b, 0x4d, 0x57, 0x32, 0x44, 0x7a, 0x75, 0x62, 0x66, 0x6c, 0x4c, 0x4c, 0x6a, 0x4b, 0x5a, 0x4e, 0x43, 0x34, 0x6f, 0x59, 0x71, 0x43, 0x4b, 0x39, 0x75, 0x6b, 0x72, 0x34, 0x78, 0x24, 0x47, 0x4b, 0x52, 0x6c, 0x71, 0x70, 0x33, 0x68, 0x68, 0x49, 0x70, 0x53, 0x69, 0x6e, 0x31, 0x71, 0x4e, 0x6d, 0x35, 0x75, 0x71, 0x43, 0x42, 0x79, 0x70, 0x6b, 0x54, 0x57, 0x6b, 0x49, 0x43, 0x4e, 0x6b, 0x6c, 0x52, 0x7a, 0x78, 0x3d, 0x5f, 0x72, 0x6d, 0x34, 0x56, 0x68, 0x5f, 0x4a, 0x64, 0x54, 0x63, 0x4e, 0x73, 0x68, 0x7a, 0x6d, 0x75, 0x5f, 0x67, 0x32, 0x62, 0x4c, 0x66, 0x59, 0x63, 0x65, 0x44, 0x45, 0x78, 0x39, 0x45, 0x4f, 0x32, 0x62, 0x37, 0x43, 0x43, 0x6f, 0x73, 0x37, 0x36, 0x71, 0x51, 0x63, 0x61, 0x35, 0x79, 0x6f, 0x37, 0x6c, 0x4d, 0x7a, 0x62, 0x73, 0x5f, 0x35, 0x6c, 0x48, 0x6f, 0x77, 0x67, 0x78, 0x29, 0x70, 0x5f, 0x43, 0x6b, 0x67, 0x66, 0x6a, 0x68, 0x72, 0x54, 0x70, 0x68, 0x57, 0x7a, 0x32, 0x6a, 0x55, 0x48, 0x56, 0x6f, 0x52, 0x4a, 0x70, 0x5a, 0x6f, 0x52, 0x70, 0x62, 0x5a, 0x38, 0x31, 0x33, 0x50, 0x32, 0x35, 0x54, 0x53, 0x79, 0x51, 0x37, 0x6c, 0x78, 0x44, 0x54, 0x6a, 0x4c, 0x56, 0x65, 0x50, 0x4a, 0x34, 0x67, 0x30, 0x62, 0x55, 0x65, 0x42, 0x48, 0x4b, 0x69, 0x62, 0x6a, 0x48, 0x57, 0x59, 0x43, 0x31, 0x62, 0x6e, 0x5f, 0x38, 0x4d, 0x39, 0x62, 0x46, 0x77, 0x53, 0x48, 0x69, 0x37, 0x62, 0x72, 0x48, 0x55, 0x6d, 0x6e, 0x4a, 0x54, 0x47, 0x58, 0x4a, 0x32, 0x4a, 0x31, 0x63, 0x33, 0x68, 0x48, 0x4b, 0x4c, 0x79, 0x6d, 0x77, 0x68, 0x45, 0x62, 0x6f, 0x45, 0x38, 0x74, 0x62, 0x78, 0x1a, 0x76, 0x6b, 0x5a, 0x73, 0x68, 0x79, 0x42, 0x32, 0x73, 0x42, 0x53, 0x69, 0x48, 0x64, 0x32, 0x56, 0x31, 0x7a, 0x33, 0x36, 0x74, 0x63, 0x4f, 0x47, 0x54, 0x37, 0x78, 0x5c, 0x70, 0x47, 0x65, 0x6c, 0x73, 0x50, 0x32, 0x66, 0x58, 0x45, 0x6d, 0x32, 0x33, 0x37, 0x36, 0x64, 0x48, 0x76, 0x4a, 0x74, 0x79, 0x56, 0x6a, 0x5a, 0x63, 0x54, 0x4f, 0x33, 0x67, 0x31, 0x56, 0x36, 0x4c, 0x6c, 0x50, 0x44, 0x67, 0x71, 0x37, 0x6b, 0x63, 0x61, 0x66, 0x46, 0x6a, 0x6b, 0x5a, 0x67, 0x49, 0x72, 0x53, 0x44, 0x46, 0x38, 0x78, 0x71, 0x41, 0x66, 0x79, 0x70, 0x45, 0x33, 0x6d, 0x52, 0x64, 0x43, 0x6f, 0x76, 0x78, 0x35, 0x43, 0x70, 0x47, 0x6d, 0x76, 0x41, 0x4e, 0x77, 0x79, 0x52, 0x74, 0x56, 0x7a, 0x32, 0x73, 0x50, 0x6a, 0x55, 0x52, 0x49, 0x44, 0x69, 0x6e, 0x59, 0x76, 0x65, 0x5a, 0x51, 0x79, 0x32, 0x69, 0x31, 0x79, 0x36, 0x34, 0x73, 0x72, 0x78, 0x32, 0x68, 0x34, 0x41, 0x4e, 0x6b, 0x35, 0x62, 0x56, 0x64, 0x71, 0x59, 0x31, 0x4b, 0x6a, 0x73, 0x53, 0x59, 0x52, 0x51, 0x6e, 0x64, 0x41, 0x5a, 0x6d, 0x52, 0x6e, 0x47, 0x49, 0x42, 0x4a, 0x4a, 0x69, 0x50, 0x7a, 0x76, 0x69, 0x67, 0x53, 0x5f, 0x6c, 0x6d, 0x58, 0x72, 0x73, 0x69, 0x4f, 0x6e, 0x79, 0x38, 0x74, 0x71, 0x54, 0x6d, 0x35, 0x34, 0x41, 0x35, 0x6f, 0x42, 0x37, 0x77, 0x70, 0x35, 0x59, 0x74, 0x72, 0x50, 0x49, 0x74, 0x54, 0x51, 0x72, 0x4e, 0x56, 0x6d, 0x35, 0x50, 0x56, 0x66, 0x58, 0x39, 0x48, 0x5a, 0x42, 0x4b, 0x78, 0x45, 0x62, 0x35, 0x67, 0x4b, 0x53, 0x47, 0x38, 0x31, 0x61, 0x71, 0x78, 0x6f, 0x7a, 0x4d, 0x44, 0x69, 0x75, 0x58, 0x46, 0x68, 0x45, 0x30, 0x4d, 0x30, 0x66, 0x62, 0x45, 0x30, 0x53, 0x7a, 0x61, 0x55, 0x6c, 0x72, 0x5f, 0x77, 0x72, 0x73, 0x4d, 0x65, 0x30, 0x43, 0x35, 0x53, 0x32, 0x4f, 0x68, 0x55, 0x70, 0x35, 0x6e, 0x74, 0x69, 0x57, 0x63, 0x35, 0x73, 0x33, 0x76, 0x6b, 0x32, 0x43, 0x67, 0x45, 0x79, 0x50, 0x62, 0x43, 0x31, 0x63, 0x6f, 0x79, 0x41, 0x68, 0x75, 0x32, 0x58, 0x62, 0x51, 0x71, 0x31, 0x4b, 0x6e, 0x72, 0x41, 0x4c, 0x5a, 0x55, 0x45, 0x67, 0x61, 0x5f, 0x48, 0x4e, 0x7a, 0x59, 0x72, 0x4b, 0x75, 0x4e, 0x64, 0x54, 0x55, 0x65, 0x7a, 0x72, 0x45, 0x32, 0x6c, 0x6b, 0x56, 0x63, 0x71, 0x63, 0x50, 0x68, 0x30, 0x74, 0x37, 0x63, 0x43, 0x61, 0x33, 0x6c, 0x71, 0x57, 0x62, 0x44, 0x64, 0x6a, 0x36, 0x32, 0x6c, 0x69, 0x37, 0x32, 0x78, 0x22, 0x4a, 0x5f, 0x68, 0x33, 0x78, 0x47, 0x4b, 0x46, 0x43, 0x54, 0x68, 0x6d, 0x72, 0x4e, 0x31, 0x31, 0x46, 0x71, 0x69, 0x49, 0x65, 0x77, 0x74, 0x30, 0x5f, 0x50, 0x67, 0x58, 0x38, 0x74, 0x34, 0x72, 0x6a, 0x53, 0x6b, 0x36, 0x4c, 0x6d, 0x55, 0x39, 0x56, 0x36, 0x59, 0x79, 0x4c, 0x68, 0x78, 0x20, 0x77, 0x53, 0x66, 0x66, 0x6d, 0x66, 0x78, 0x52, 0x48, 0x69, 0x6c, 0x57, 0x59, 0x47, 0x78, 0x48, 0x79, 0x6c, 0x78, 0x4b, 0x49, 0x64, 0x41, 0x55, 0x64, 0x74, 0x7a, 0x30, 0x58, 0x31, 0x75, 0x42, 0x78, 0x2c, 0x48, 0x51, 0x71, 0x72, 0x39, 0x44, 0x75, 0x6f, 0x78, 0x30, 0x36, 0x6f, 0x70, 0x30, 0x79, 0x57, 0x34, 0x6e, 0x62, 0x4a, 0x66, 0x5f, 0x38, 0x41, 0x6c, 0x50, 0x6a, 0x63, 0x4b, 0x61, 0x75, 0x30, 0x54, 0x61, 0x4d, 0x6b, 0x55, 0x65, 0x5f, 0x32, 0x65, 0x5a, 0x4f, 0x77, 0xa4, 0x61, 0x77, 0x69, 0x46, 0x72, 0x67, 0x6b, 0x68, 0x5a, 0x4a, 0x65, 0x4c, 0x62, 0x76, 0x54, 0x63, 0x6c, 0x56, 0x76, 0x63, 0x62, 0x6b, 0x54, 0x63, 0x50, 0x74, 0x4d, 0x62, 0x76, 0x73, 0x6b, 0x30, 0x62, 0x70, 0x5a, 0x54, 0x70, 0x51, 0x39, 0x30, 0x4c, 0x4c, 0x78, 0x1f, 0x77, 0x61, 0x43, 0x41, 0x55, 0x63, 0x73, 0x73, 0x5a, 0x43, 0x62, 0x4a, 0x6c, 0x72, 0x42, 0x31, 0x76, 0x4d, 0x56, 0x34, 0x49, 0x71, 0x30, 0x48, 0x63, 0x30, 0x35, 0x50, 0x65, 0x49, 0x6d, 0x78, 0x3c, 0x41, 0x55, 0x77, 0x76, 0x73, 0x70, 0x76, 0x38, 0x6d, 0x6d, 0x36, 0x6f, 0x6d, 0x4b, 0x4c, 0x35, 0x6b, 0x55, 0x48, 0x34, 0x39, 0x45, 0x53, 0x43, 0x50, 0x4e, 0x69, 0x44, 0x4f, 0x36, 0x63, 0x59, 0x71, 0x6f, 0x33, 0x48, 0x63, 0x55, 0x5a, 0x55, 0x56, 0x65, 0x73, 0x55, 0x30, 0x62, 0x4a, 0x45, 0x39, 0x79, 0x45, 0x34, 0x4c, 0x4b, 0x6a, 0x51, 0x51, 0x69, 0x75, 0x67, 0x62, 0x49, 0x4c, 0x78, 0x32, 0x6e, 0x78, 0x44, 0x56, 0x47, 0x63, 0x78, 0x46, 0x42, 0x62, 0x35, 0x58, 0x70, 0x39, 0x73, 0x68, 0x52, 0x5f, 0x39, 0x72, 0x66, 0x4c, 0x5a, 0x34, 0x6d, 0x52, 0x53, 0x65, 0x39, 0x6a, 0x34, 0x65, 0x31, 0x38, 0x72, 0x66, 0x43, 0x70, 0x32, 0x79, 0x35, 0x79, 0x65, 0x6b, 0x73, 0x47, 0x4b, 0x71, 0x65, 0x53, 0x78, 0x1d, 0x79, 0x36, 0x41, 0x37, 0x51, 0x79, 0x4f, 0x38, 0x34, 0x41, 0x72, 0x51, 0x67, 0x66, 0x57, 0x39, 0x75, 0x4d, 0x44, 0x44, 0x56, 0x76, 0x50, 0x61, 0x4e, 0x43, 0x5f, 0x42, 0x6e, 0xa3, 0x61, 0x6b, 0x62, 0x74, 0x38, 0x66, 0x6b, 0x46, 0x6d, 0x73, 0x72, 0x46, 0xa1, 0x61, 0x6c, 0x67, 0x57, 0x47, 0x33, 0x38, 0x46, 0x41, 0x53, 0x61, 0x4c, 0x64, 0x31, 0x5a, 0x30, 0x62, 0x65, 0x45, 0x71, 0x76, 0x54, 0x6f, 0x78, 0x23, 0x6d, 0x6c, 0x6f, 0x50, 0x76, 0x73, 0x31, 0x38, 0x4a, 0x67, 0x33, 0x4f, 0x53, 0x57, 0x5f, 0x34, 0x49, 0x63, 0x46, 0x58, 0x50, 0x45, 0x6f, 0x33, 0x63, 0x73, 0x55, 0x6e, 0x76, 0x6c, 0x43, 0x6c, 0x6c, 0x36, 0x79, 0x78, 0x1e, 0x39, 0x30, 0x30, 0x66, 0x38, 0x54, 0x31, 0x45, 0x47, 0x4c, 0x73, 0x63, 0x4f, 0x4f, 0x6d, 0x4a, 0x35, 0x6f, 0x37, 0x55, 0x5f, 0x44, 0x50, 0x68, 0x72, 0x50, 0x67, 0x73, 0x7a, 0x77, 0x78, 0x40, 0x45, 0x5a, 0x4c, 0x34, 0x47, 0x6a, 0x51, 0x37, 0x65, 0x58, 0x38, 0x48, 0x78, 0x33, 0x5a, 0x45, 0x4c, 0x31, 0x50, 0x37, 0x54, 0x73, 0x4a, 0x54, 0x53, 0x61, 0x37, 0x77, 0x66, 0x34, 0x69, 0x6a, 0x79, 0x33, 0x31, 0x49, 0x76, 0x4f, 0x7a, 0x4a, 0x43, 0x39, 0x76, 0x38, 0x39, 0x57, 0x36, 0x47, 0x44, 0x4b, 0x67, 0x45, 0x7a, 0x4f, 0x79, 0x44, 0x68, 0x68, 0x75, 0x76, 0x42, 0x35, 0x69, 0x6a, 0x6e, 0x41, 0x34, 0x6e, 0x50, 0x4f, 0x34, 0x32, 0x48, 0x44, 0x35, 0x46, 0x43, 0x51, 0x4d, 0xa2, 0x61, 0x57, 0x68, 0x71, 0x6d, 0x79, 0x5f, 0x77, 0x62, 0x4b, 0x37, 0x64, 0x48, 0x36, 0x38, 0x71, 0xa1, 0x61, 0x33, 0x64, 0x6d, 0x77, 0x38, 0x7a, 0x6a, 0x37, 0x69, 0x74, 0x65, 0x6c, 0x31, 0x4c, 0x67, 0x41, 0x6b, 0xa3, 0x61, 0x39, 0x63, 0x75, 0x64, 0x76, 0x64, 0x49, 0x67, 0x6a, 0x71, 0x6a, 0x37, 0x43, 0x75, 0x38, 0x72, 0x77, 0x41, 0x5a, 0x30, 0x48, 0x61, 0x6f, 0x6f, 0x7a, 0x4d, 0x76, 0x5a, 0x79, 0x46, 0x32, 0x79, 0x4a, 0x78, 0x56, 0x51, 0x47, 0x6b, 0x6d, 0x78, 0x1a, 0x46, 0x79, 0x73, 0x42, 0x4c, 0x31, 0x35, 0x50, 0x35, 0x72, 0x63, 0x64, 0x52, 0x62, 0x6c, 0x76, 0x56, 0x67, 0x69, 0x44, 0x4f, 0x65, 0x78, 0x4f, 0x39, 0x76, 0xa4, 0x61, 0x56, 0x6a, 0x73, 0x57, 0x6e, 0x49, 0x55, 0x41, 0x61, 0x52, 0x46, 0x6b, 0x61, 0x69, 0xa1, 0x61, 0x59, 0x65, 0x31, 0x51, 0x45, 0x46, 0x39, 0x64, 0x64, 0x62, 0x4f, 0x6e, 0x6b, 0x4e, 0x78, 0x6d, 0x63, 0x50, 0x76, 0x52, 0x48, 0x42, 0x50, 0x51, 0x63, 0x4b, 0x62, 0x58, 0x64, 0x65, 0x4c, 0x50, 0x4f, 0x6f, 0x75, 0x30, 0x73, 0x76, 0x42, 0x59, 0x4b, 0x6c, 0x43, 0x6f, 0x5a, 0x75, 0x56, 0x68, 0x61, 0x78, 0x54, 0x39, 0x4d, 0x4d, 0x64, 0x54, 0x76, 0x77, 0x51, 0x37, 0x4b, 0x6f, 0x5f, 0x78, 0x56, 0x6f, 0x61, 0x6d, 0x50, 0x58, 0x6c, 0x48, 0x77, 0x5a, 0x6e, 0x4a, 0x74, 0x37, 0x51, 0x47, 0x6b, 0x34, 0x46, 0x70, 0x33, 0x6b, 0x72, 0x33, 0x58, 0x48, 0x4b, 0x6d, 0x4f, 0x42, 0x51, 0x37, 0x71, 0x7a, 0x36, 0x72, 0x5a, 0x35, 0x66, 0x41, 0x66, 0x61, 0x5a, 0x52, 0x79, 0x30, 0x6d, 0x4e, 0x47, 0x5a, 0x59, 0x62, 0x5f, 0x54, 0x42, 0x79, 0x6c, 0x55, 0x79, 0x74, 0x63, 0x5a, 0x73, 0x46, 0x4f, 0x74, 0x32, 0x45, 0x6a, 0x33, 0x5f, 0x78, 0x32, 0x6f, 0x6b, 0x63, 0x32, 0x61, 0x49, 0x36, 0x64, 0x71, 0x61, 0x32, 0x4c, 0x64, 0x79, 0x39, 0x30, 0x48, 0x52, 0x63, 0x7a, 0x59, 0x4a, 0x51, 0x4f, 0x74, 0x63, 0x35, 0x4d, 0x74, 0x35, 0x42, 0x6a, 0x56, 0x44, 0x75, 0x47, 0x78, 0x31, 0x75, 0x66, 0x33, 0x33, 0x5f, 0x74, 0x62, 0x62, 0x72, 0x58, 0x42, 0x6c, 0xa4, 0x61, 0x64, 0x69, 0x4b, 0x5f, 0x58, 0x74, 0x47, 0x4d, 0x75, 0x4f, 0x55, 0x64, 0x56, 0x33, 0x72, 0x55, 0x6c, 0x6e, 0x4b, 0x73, 0x4b, 0x6a, 0x71, 0x32, 0x4a, 0x49, 0x4c, 0x43, 0x7a, 0x61, 0x6f, 0x66, 0x39, 0x64, 0x31, 0x66, 0x78, 0x79, 0x65, 0x51, 0x30, 0x50, 0x30, 0x62, 0xa1, 0x61, 0x51, 0x63, 0x37, 0x7a, 0x75, 0x67, 0x50, 0x56, 0x4a, 0x7a, 0x4b, 0x5f, 0x71, 0xa4, 0x61, 0x44, 0x6d, 0x64, 0x42, 0x59, 0x76, 0x34, 0x36, 0x71, 0x53, 0x67, 0x72, 0x51, 0x36, 0x45, 0x66, 0x6c, 0x69, 0x30, 0x48, 0x6d, 0x67, 0x6c, 0x66, 0x69, 0x72, 0x4c, 0x63, 0x56, 0x71, 0x45, 0x6b, 0x36, 0x68, 0x72, 0x63, 0x63, 0x47, 0x53, 0xa1, 0x61, 0x67, 0x61, 0x7a, 0x62, 0x58, 0x74, 0x6c, 0x65, 0x36, 0x6e, 0x79, 0x6d, 0x50, 0x65, 0x49, 0x72, 0x5a, 0x56, 0x35, 0x78, 0x2f, 0x44, 0x51, 0x63, 0x57, 0x63, 0x77, 0x70, 0x50, 0x64, 0x78, 0x78, 0x7a, 0x6d, 0x62, 0x38, 0x62, 0x55, 0x5a, 0x7a, 0x61, 0x41, 0x7a, 0x41, 0x5f, 0x66, 0x77, 0x51, 0x53, 0x6a, 0x4b, 0x78, 0x4b, 0x50, 0x31, 0x69, 0x5a, 0x4a, 0x48, 0x62, 0x53, 0x42, 0x56, 0x76, 0x39, 0x58, 0x49, 0x61, 0x78, 0x2e, 0x62, 0x78, 0x71, 0x6e, 0x6d, 0x54, 0x72, 0x62, 0x4b, 0x38, 0x72, 0x5a, 0x5f, 0x33, 0x77, 0x6e, 0x6b, 0x78, 0x54, 0x75, 0x51, 0x33, 0x4e, 0x6b, 0x4a, 0x72, 0x73, 0x41, 0x37, 0x74, 0x79, 0x31, 0x6a, 0x61, 0x30, 0x52, 0x65, 0x70, 0x49, 0x4f, 0x45, 0x51, 0x50, 0x4d, 0x47, 0x4e, 0x78, 0x18, 0x37, 0x7a, 0x57, 0x62, 0x74, 0x72, 0x46, 0x34, 0x62, 0x61, 0x35, 0x44, 0x30, 0x5f, 0x5f, 0x61, 0x66, 0x48, 0x63, 0x52, 0x66, 0x75, 0x63, 0x47, 0x78, 0x56, 0x68, 0x65, 0x65, 0x67, 0x6b, 0x46, 0x4c, 0x52, 0x66, 0x5f, 0x38, 0x34, 0x6a, 0x79, 0x61, 0x6b, 0x42, 0x4a, 0x4e, 0x35, 0x32, 0x30, 0x6c, 0x32, 0x65, 0x66, 0x33, 0x48, 0x75, 0x35, 0x76, 0x30, 0x5a, 0x37, 0x41, 0x4c, 0x73, 0x6d, 0x6d, 0x5f, 0x74, 0x41, 0x66, 0x45, 0x41, 0x38, 0x33, 0x44, 0x7a, 0x71, 0x31, 0x62, 0x74, 0x4b, 0x65, 0x32, 0x61, 0x34, 0x48, 0x4b, 0x6e, 0x4e, 0x68, 0x71, 0x54, 0x36, 0x78, 0x39, 0x35, 0x53, 0x35, 0x48, 0x6a, 0x76, 0x38, 0x42, 0x39, 0x56, 0x54, 0x45, 0x4c, 0x6d, 0x65, 0x63, 0x42, 0x64, 0x78, 0x25, 0x57, 0x61, 0x62, 0x35, 0x50, 0x6a, 0x4e, 0x4f, 0x76, 0x64, 0x4d, 0x77, 0x76, 0x7a, 0x32, 0x77, 0x41, 0x6c, 0x70, 0x4c, 0x68, 0x55, 0x31, 0x35, 0x59, 0x31, 0x31, 0x77, 0x75, 0x79, 0x74, 0x6f, 0x6b, 0x74, 0x69, 0x38, 0x56, 0x78, 0x2a, 0x41, 0x6e, 0x31, 0x50, 0x44, 0x63, 0x37, 0x45, 0x64, 0x4a, 0x66, 0x43, 0x6c, 0x68, 0x6e, 0x38, 0x6c, 0x79, 0x4c, 0x54, 0x36, 0x31, 0x57, 0x5a, 0x77, 0x6a, 0x49, 0x53, 0x6a, 0x75, 0x31, 0x44, 0x49, 0x53, 0x69, 0x56, 0x33, 0x61, 0x61, 0x65, 0x50, 0x52, 0x63, 0x4b, 0x36, 0x47, 0x78, 0x4a, 0x51, 0x49, 0x65, 0x39, 0x66, 0x64, 0x70, 0x34, 0x6e, 0x56, 0x69, 0x68, 0x73, 0x61, 0x6b, 0x35, 0x46, 0x47, 0x48, 0x49, 0x75, 0x70, 0x6c, 0x63, 0x77, 0x59, 0x52, 0x30, 0x44, 0x61, 0x5a, 0x6f, 0x4f, 0x70, 0x6f, 0x58, 0x6d, 0x59, 0x31, 0x45, 0x49, 0x62, 0x52, 0x42, 0x75, 0x64, 0x70, 0x39, 0x35, 0x6c, 0x32, 0x4f, 0x4b, 0x49, 0x52, 0x4f, 0x62, 0x66, 0x79, 0x78, 0x58, 0x46, 0x79, 0x77, 0x73, 0x52, 0x77, 0x6d, 0x52, 0x33, 0x33, 0x38, 0x44, 0x56, 0x69, 0x49, 0x4e, 0x6a, 0x78, 0x31, 0x71, 0x33, 0x46, 0x72, 0x78, 0x1e, 0x34, 0x57, 0x53, 0x58, 0x50, 0x52, 0x58, 0x54, 0x36, 0x69, 0x4b, 0x30, 0x5f, 0x44, 0x6e, 0x4c, 0x59, 0x6e, 0x67, 0x59, 0x73, 0x33, 0x58, 0x51, 0x58, 0x77, 0x45, 0x41, 0x5a, 0x49, 0x78, 0x1a, 0x49, 0x4c, 0x79, 0x36, 0x4a, 0x49, 0x39, 0x49, 0x33, 0x38, 0x4f, 0x69, 0x57, 0x55, 0x78, 0x70, 0x38, 0x71, 0x37, 0x55, 0x39, 0x79, 0x66, 0x62, 0x77, 0x52, 0x78, 0x1d, 0x73, 0x4e, 0x4c, 0x6b, 0x30, 0x47, 0x65, 0x73, 0x65, 0x4e, 0x78, 0x5a, 0x33, 0x56, 0x49, 0x4e, 0x6c, 0x68, 0x4b, 0x6d, 0x53, 0x35, 0x41, 0x4d, 0x72, 0x59, 0x34, 0x6b, 0x59, 0x62, 0x72, 0x58, 0x78, 0x23, 0x6f, 0x57, 0x33, 0x67, 0x51, 0x7a, 0x54, 0x6c, 0x59, 0x6c, 0x37, 0x47, 0x30, 0x31, 0x5a, 0x72, 0x55, 0x75, 0x46, 0x63, 0x33, 0x6e, 0x50, 0x36, 0x6d, 0x51, 0x54, 0x47, 0x59, 0x78, 0x4f, 0x4a, 0x37, 0x67, 0x44, 0x78, 0x19, 0x66, 0x77, 0x6c, 0x34, 0x32, 0x48, 0x54, 0x30, 0x57, 0x4c, 0x6b, 0x6d, 0x78, 0x46, 0x76, 0x36, 0x77, 0x42, 0x6b, 0x45, 0x57, 0x75, 0x46, 0x68, 0x6d, 0x78, 0x1b, 0x72, 0x36, 0x39, 0x4e, 0x61, 0x42, 0x67, 0x61, 0x71, 0x67, 0x63, 0x6a, 0x35, 0x4e, 0x56, 0x6f, 0x75, 0x49, 0x70, 0x32, 0x39, 0x71, 0x4e, 0x72, 0x49, 0x30, 0x39, 0x78, 0x22, 0x49, 0x55, 0x75, 0x36, 0x70, 0x6a, 0x5a, 0x54, 0x56, 0x66, 0x56, 0x6a, 0x70, 0x41, 0x76, 0x4e, 0x4d, 0x74, 0x45, 0x61, 0x55, 0x6d, 0x63, 0x6d, 0x39, 0x65, 0x6a, 0x49, 0x68, 0x70, 0x43, 0x76, 0x6f, 0x7a, 0x78, 0x4e, 0x6c, 0x57, 0x55, 0x64, 0x31, 0x5a, 0x78, 0x35, 0x4a, 0x45, 0x53, 0x31, 0x35, 0x61, 0x33, 0x56, 0x68, 0x57, 0x32, 0x34, 0x53, 0x6f, 0x61, 0x46, 0x51, 0x30, 0x51, 0x68, 0x53, 0x5a, 0x6d, 0x57, 0x76, 0x7a, 0x30, 0x6b, 0x69, 0x59, 0x6b, 0x34, 0x72, 0x55, 0x58, 0x58, 0x34, 0x43, 0x39, 0x6e, 0x6a, 0x49, 0x6d, 0x6a, 0x77, 0x66, 0x70, 0x49, 0x41, 0x69, 0x4d, 0x36, 0x55, 0x6f, 0x57, 0x38, 0x78, 0x44, 0x46, 0x77, 0x4e, 0x61, 0x77, 0x37, 0x77, 0x75, 0x33, 0x64, 0x74, 0x6e, 0x63, 0x71, 0x69, 0x78, 0xa3, 0x61, 0x4a, 0x66, 0x37, 0x57, 0x55, 0x48, 0x4f, 0x39, 0x63, 0x6f, 0x42, 0x71, 0x64, 0x47, 0x45, 0x6d, 0x72, 0x64, 0x51, 0x6b, 0x44, 0x6c, 0x63, 0x58, 0x54, 0x44, 0x78, 0x25, 0x58, 0x69, 0x51, 0x75, 0x72, 0x44, 0x7a, 0x52, 0x31, 0x41, 0x56, 0x6b, 0x5a, 0x6c, 0x70, 0x53, 0x55, 0x78, 0x36, 0x71, 0x51, 0x6e, 0x61, 0x6c, 0x61, 0x7a, 0x5a, 0x57, 0x4f, 0x33, 0x75, 0x50, 0x33, 0x66, 0x32, 0x71, 0x32, 0x78, 0x38, 0x78, 0x6d, 0x4d, 0x32, 0x45, 0x30, 0x53, 0x39, 0x55, 0x5a, 0x39, 0x55, 0x6f, 0x66, 0x45, 0x6f, 0x67, 0x45, 0x36, 0x38, 0x4c, 0x32, 0x50, 0x54, 0x43, 0x45, 0x32, 0x4f, 0x4d, 0x38, 0x78, 0x61, 0x77, 0x6f, 0x4d, 0x36, 0x67, 0x6e, 0x55, 0x65, 0x4d, 0x75, 0x49, 0x7a, 0x74, 0x68, 0x50, 0x4a, 0x6a, 0x50, 0x56, 0x6a, 0x37, 0x6f, 0x58, 0x4f, 0x6e, 0x51, 0x52, 0x70, 0x54, 0x31, 0x69, 0x63, 0x69, 0x56, 0x30, 0x6c, 0x6c, 0x78, 0x6a, 0x78, 0x1b, 0x31, 0x68, 0x4f, 0x57, 0x74, 0x72, 0x44, 0x4c, 0x6a, 0x30, 0x77, 0x4b, 0x71, 0x39, 0x32, 0x4a, 0x44, 0x6b, 0x61, 0x48, 0x35, 0x6e, 0x38, 0x41, 0x68, 0x56, 0x35, 0x72, 0x6c, 0x6e, 0x6d, 0x4e, 0x58, 0x6a, 0x37, 0x52, 0x70, 0x61, 0x5f, 0x37, 0x5f, 0x70, 0x48, 0x58, 0x69, 0x75, 0x78, 0x23, 0x73, 0x76, 0x57, 0x46, 0x58, 0x47, 0x67, 0x7a, 0x70, 0x5a, 0x70, 0x59, 0x4d, 0x38, 0x65, 0x62, 0x59, 0x38, 0x33, 0x46, 0x4b, 0x4a, 0x62, 0x57, 0x61, 0x78, 0x37, 0x6c, 0x36, 0x6f, 0x66, 0x58, 0x35, 0x49, 0x50, 0x73, 0x52, 0x57, 0x47, 0x44, 0x51, 0x4d, 0x64, 0x4f, 0x31, 0x31, 0x39, 0x70, 0x56, 0x74, 0x55, 0x4b, 0x32, 0x74, 0x57, 0x78, 0x33, 0x65, 0x68, 0x44, 0x6b, 0x56, 0x55, 0x72, 0x54, 0x36, 0x54, 0x54, 0x52, 0x53, 0x65, 0x66, 0x6d, 0x4f, 0x63, 0x6c, 0x38, 0x4e, 0x79, 0x33, 0x55, 0x50, 0x71, 0x61, 0x51, 0x35, 0x41, 0x67, 0x39, 0x68, 0x46, 0x47, 0x6e, 0x54, 0x39, 0x49, 0x62, 0x36, 0x73, 0x72, 0x58, 0x4b, 0x51, 0x62, 0x79, 0x61, 0x68, 0x32, 0x65, 0x36, 0x33, 0x53, 0x63, 0x54, 0x78, 0x52, 0x39, 0x36, 0x6c, 0x46, 0x7a, 0x65, 0x68, 0x6e, 0x30, 0x57, 0x42, 0x41, 0x71, 0x4e, 0x33, 0x38, 0x75, 0x42, 0x33, 0x7a, 0x6f, 0x6d, 0x32, 0x30, 0x32, 0x77, 0x67, 0x6e, 0x43, 0x5f, 0x73, 0x57, 0x57, 0x58, 0x4f, 0x70, 0x4b, 0x42, 0x57, 0x59, 0x30, 0x33, 0x44, 0x4c, 0x5a, 0x57, 0x4d, 0x48, 0x30, 0x68, 0x4c, 0x73, 0x31, 0x47, 0x6d, 0x33, 0x6e, 0x31, 0x45, 0x54, 0x7a, 0x50, 0x6f, 0x55, 0x78, 0x75, 0x53, 0x59, 0x6d, 0x64, 0x64, 0x39, 0x63, 0x77, 0x41, 0x67, 0x49, 0x69, 0x65, 0x38, 0x79, 0x74, 0x67, 0x6c, 0x43, 0x78, 0x49, 0x58, 0x5f, 0x38, 0xa5, 0x61, 0x46, 0x63, 0x63, 0x62, 0x4e, 0x63, 0x50, 0x33, 0x6d, 0x6f, 0x49, 0x72, 0x5f, 0x62, 0x59, 0x6e, 0x48, 0x4d, 0x52, 0x69, 0x4a, 0x65, 0x36, 0x49, 0x42, 0x62, 0x74, 0x7a, 0x71, 0x34, 0x4e, 0x4f, 0x4a, 0x55, 0x46, 0x6f, 0x54, 0x58, 0x37, 0x77, 0x56, 0x78, 0x46, 0x7a, 0x64, 0x51, 0x61, 0x42, 0x65, 0x45, 0x63, 0x68, 0x41, 0x69, 0x65, 0x65, 0x54, 0x39, 0x6d, 0x6b, 0x64, 0x6c, 0x73, 0x78, 0x52, 0x78, 0x24, 0x33, 0x48, 0x64, 0x6e, 0x47, 0x4f, 0x39, 0x68, 0x64, 0x41, 0x75, 0x54, 0x65, 0x5a, 0x31, 0x72, 0x64, 0x64, 0x43, 0x70, 0x34, 0x4b, 0x70, 0x6d, 0x57, 0x46, 0x61, 0x69, 0x33, 0x51, 0x73, 0x6e, 0x69, 0x77, 0x5f, 0x53, 0x78, 0x4c, 0x52, 0x75, 0x45, 0x4f, 0x6d, 0x61, 0x6b, 0x79, 0x39, 0x31, 0x67, 0x4b, 0x79, 0x6b, 0x70, 0x52, 0x71, 0x76, 0x6b, 0x31, 0x38, 0x6a, 0x64, 0x38, 0x6d, 0x68, 0x32, 0x6f, 0x68, 0x32, 0x41, 0x6c, 0x36, 0x43, 0x38, 0x42, 0x41, 0x79, 0x79, 0x75, 0x72, 0x77, 0x7a, 0x65, 0x36, 0x73, 0x69, 0x68, 0x5a, 0x34, 0x31, 0x6c, 0x58, 0x48, 0x7a, 0x4a, 0x47, 0x36, 0x6a, 0x49, 0x30, 0x41, 0x69, 0x71, 0x4b, 0x63, 0x33, 0x6a, 0x34, 0x79, 0x76, 0x53, 0x4c, 0x73, 0x4d, 0x37, 0x68, 0x78, 0x76, 0x76, 0x77, 0x69, 0x64, 0x52, 0x65, 0x78, 0x1b, 0x53, 0x4a, 0x6a, 0x61, 0x4e, 0x76, 0x43, 0x54, 0x49, 0x78, 0x79, 0x69, 0x74, 0x76, 0x44, 0x41, 0x67, 0x67, 0x71, 0x41, 0x61, 0x74, 0x47, 0x44, 0x36, 0x31, 0x56, 0x64, 0x58, 0x64, 0x38, 0x53, 0xa3, 0x61, 0x4e, 0x65, 0x75, 0x42, 0x78, 0x4f, 0x62, 0x64, 0x72, 0x65, 0x6b, 0x6a, 0x64, 0x46, 0x31, 0x51, 0x44, 0x66, 0x4b, 0x32, 0x45, 0x34, 0x33, 0x43, 0x65, 0x38, 0x51, 0x31, 0x50, 0x68, 0x61, 0x63, 0x78, 0x35, 0x6d, 0x61, 0x4a, 0x34, 0x75, 0x35, 0x4a, 0x75, 0x75, 0x59, 0x73, 0x71, 0x72, 0x62, 0x39, 0x52, 0x48, 0x74, 0x68, 0x50, 0x74, 0x6d, 0x4c, 0x73, 0x42, 0x78, 0x64, 0x37, 0x53, 0x65, 0x65, 0x72, 0x6e, 0x78, 0x5a, 0x4d, 0x39, 0x55, 0x54, 0x41, 0x6f, 0x7a, 0x56, 0x58, 0x4c, 0x66, 0x33, 0x4a, 0x35, 0x66, 0x47, 0x70, 0x78, 0x78, 0x2c, 0x6e, 0x63, 0x57, 0x64, 0x51, 0x73, 0x31, 0x50, 0x79, 0x63, 0x6d, 0x4f, 0x39, 0x61, 0x4b, 0x4c, 0x53, 0x34, 0x73, 0x57, 0x73, 0x38, 0x46, 0x71, 0x35, 0x4e, 0x79, 0x4d, 0x50, 0x58, 0x53, 0x31, 0x36, 0x6b, 0x54, 0x49, 0x46, 0x35, 0x74, 0x41, 0x63, 0x4c, 0x44, 0x54, 0xa4, 0x61, 0x47, 0x6e, 0x57, 0x57, 0x34, 0x57, 0x63, 0x65, 0x6a, 0x57, 0x36, 0x45, 0x6f, 0x77, 0x41, 0x75, 0x61, 0x4f, 0x6b, 0x55, 0x57, 0x44, 0x70, 0x33, 0x47, 0x50, 0x30, 0x6a, 0x69, 0x4f, 0x63, 0x54, 0x75, 0x62, 0x63, 0x48, 0x61, 0x72, 0x64, 0x66, 0x4f, 0x6d, 0x69, 0x64, 0x63, 0x75, 0x66, 0x74, 0x78, 0x20, 0x47, 0x54, 0x66, 0x52, 0x79, 0x35, 0x34, 0x57, 0x49, 0x71, 0x4b, 0x32, 0x54, 0x42, 0x58, 0x76, 0x5f, 0x63, 0x65, 0x72, 0x79, 0x30, 0x56, 0x4c, 0x44, 0x4d, 0x61, 0x49, 0x77, 0x42, 0x6e, 0x6d, 0x78, 0x34, 0x72, 0x79, 0x67, 0x44, 0x69, 0x6a, 0x32, 0x6b, 0x6a, 0x75, 0x4b, 0x54, 0x50, 0x35, 0x69, 0x4c, 0x30, 0x6b, 0x6b, 0x74, 0x30, 0x57, 0x45, 0x33, 0x5a, 0x41, 0x6e, 0x77, 0x52, 0x74, 0x32, 0x39, 0x6c, 0x6e, 0x5f, 0x42, 0x71, 0x70, 0x55, 0x41, 0x4f, 0x38, 0x62, 0x6d, 0x67, 0x5a, 0x50, 0x57, 0x39, 0x67, 0x76, 0x74, 0x6b, 0x42, 0x42, 0x69, 0x62, 0x45, 0x57, 0x44, 0x55, 0x32, 0x62, 0x67, 0x78, 0x2c, 0x70, 0x71, 0x69, 0x37, 0x4c, 0x70, 0x6a, 0x69, 0x50, 0x57, 0x56, 0x72, 0x43, 0x6c, 0x35, 0x6c, 0x76, 0x45, 0x65, 0x71, 0x44, 0x73, 0x31, 0x65, 0x4b, 0x6d, 0x6a, 0x6d, 0x43, 0x4e, 0x44, 0x69, 0x79, 0x43, 0x57, 0x58, 0x5a, 0x69, 0x78, 0x6c, 0x62, 0x52, 0x41, 0x41, 0x6e, 0x64, 0x51, 0x77, 0x79, 0x66, 0x61, 0x61, 0x4c, 0x61, 0x44, 0x6e, 0x73, 0x76, 0x51, 0x78, 0x3c, 0x68, 0x33, 0x54, 0x47, 0x41, 0x43, 0x79, 0x70, 0x46, 0x53, 0x31, 0x66, 0x7a, 0x62, 0x5a, 0x6c, 0x69, 0x65, 0x62, 0x39, 0x74, 0x66, 0x78, 0x55, 0x36, 0x45, 0x42, 0x33, 0x74, 0x48, 0x6a, 0x66, 0x67, 0x32, 0x43, 0x46, 0x6e, 0x6f, 0x78, 0x54, 0x61, 0x61, 0x76, 0x49, 0x4c, 0x5a, 0x55, 0x38, 0x75, 0x38, 0x38, 0x73, 0x73, 0x79, 0x48, 0x30, 0x41, 0x6c, 0x4d, 0x4d, 0x78, 0x20, 0x35, 0x44, 0x4b, 0x5a, 0x36, 0x76, 0x55, 0x33, 0x31, 0x5a, 0x45, 0x5a, 0x70, 0x71, 0x62, 0x6a, 0x54, 0x43, 0x45, 0x58, 0x4a, 0x38, 0x48, 0x52, 0x33, 0x55, 0x4e, 0x39, 0x73, 0x70, 0x33, 0x61, 0xa3, 0x61, 0x65, 0x71, 0x38, 0x70, 0x38, 0x6a, 0x68, 0x72, 0x61, 0x62, 0x78, 0x41, 0x43, 0x33, 0x42, 0x70, 0x44, 0x71, 0x67, 0x64, 0x38, 0x5f, 0x4d, 0x38, 0x69, 0x58, 0x45, 0x47, 0x59, 0x46, 0x63, 0x71, 0x56, 0x6b, 0x62, 0x55, 0x6e, 0x65, 0x56, 0x59, 0x6e, 0x4a, 0x74, 0x65, 0x47, 0x5a, 0x7a, 0x62, 0x4f, 0x78, 0x20, 0x4a, 0x6b, 0x4a, 0x54, 0x38, 0x6d, 0x37, 0x65, 0x30, 0x6e, 0x55, 0x4f, 0x33, 0x5f, 0x77, 0x73, 0x31, 0x72, 0x45, 0x4b, 0x63, 0x5a, 0x79, 0x4a, 0x4e, 0x70, 0x33, 0x79, 0x73, 0x4b, 0x53, 0x68, 0x78, 0x19, 0x66, 0x56, 0x67, 0x4e, 0x36, 0x65, 0x47, 0x63, 0x59, 0x41, 0x63, 0x49, 0x6f, 0x4c, 0x56, 0x53, 0x42, 0x6d, 0x6a, 0x7a, 0x42, 0x34, 0x49, 0x47, 0x67, 0x78, 0x46, 0x4b, 0x6f, 0x48, 0x33, 0x38, 0x32, 0x62, 0x71, 0x5f, 0x63, 0x4b, 0x71, 0x56, 0x48, 0x74, 0x78, 0x59, 0x50, 0x63, 0x44, 0x4d, 0x66, 0x77, 0x33, 0x34, 0x70, 0x31, 0x42, 0x30, 0x30, 0x79, 0x4c, 0x75, 0x59, 0x41, 0x38, 0x5f, 0x4b, 0x68, 0x50, 0x68, 0x70, 0x59, 0x70, 0x6a, 0x6d, 0x65, 0x33, 0x62, 0x36, 0x45, 0x6e, 0x54, 0x6a, 0x46, 0x52, 0x79, 0x58, 0x68, 0x53, 0x58, 0x58, 0x49, 0x51, 0x39, 0x75, 0x4a, 0x45, 0x46, 0x4c, 0x76, 0x55, 0x58, 0x34, 0x70, 0x56, 0x36, 0x31, 0x34, 0x4a, 0x46, 0x42, 0x41, 0x61, 0x75, 0x39, 0x68, 0x6b, 0x73, 0x50, 0x75, 0x31, 0x6d, 0x78, 0x1d, 0x32, 0x4f, 0x71, 0x79, 0x72, 0x6d, 0x4d, 0x6e, 0x79, 0x48, 0x58, 0x53, 0x4f, 0x34, 0x74, 0x74, 0x54, 0x69, 0x5f, 0x5a, 0x4d, 0x51, 0x31, 0x54, 0x34, 0x77, 0x41, 0x6e, 0x6d, 0x71, 0x66, 0x32, 0x30, 0x42, 0x76, 0x37, 0x68, 0x55, 0x61, 0x33, 0x55, 0x71, 0x44, 0x36, 0x41, 0x62, 0x4a, 0xa4, 0x61, 0x49, 0x60, 0x65, 0x71, 0x4f, 0x4b, 0x61, 0x54, 0xa1, 0x61, 0x41, 0x63, 0x42, 0x4f, 0x68, 0x63, 0x51, 0x50, 0x6d, 0x62, 0x5a, 0x57, 0x67, 0x78, 0x49, 0x42, 0x56, 0x52, 0x6f, 0x55, 0x67, 0x62, 0x6a, 0x46, 0x46, 0x61, 0x57, 0x44, 0x6e, 0x41, 0x74, 0x59, 0x47, 0x6b, 0x5f, 0x44, 0x79, 0x77, 0x79, 0x64, 0x6f, 0x55, 0x63, 0x78, 0x41, 0x70, 0x73, 0x66, 0x67, 0x4a, 0x64, 0x55, 0x7a, 0x58, 0x43, 0x35, 0x64, 0x75, 0x51, 0x78, 0x65, 0x49, 0x4a, 0x75, 0x4b, 0x47, 0x71, 0x51, 0x55, 0x31, 0x38, 0x62, 0x35, 0x4c, 0x49, 0x69, 0x31, 0x63, 0x34, 0x78, 0x42, 0x45, 0x57, 0x54, 0x45, 0x55, 0x5a, 0x53, 0x47, 0x62, 0x4b, 0x36, 0x58, 0x56, 0x37, 0x57, 0x37, 0x39, 0x72, 0x64, 0x54, 0x32, 0x37, 0x37, 0x45, 0x65, 0x77, 0x47, 0x6a, 0x34, 0x6b, 0x30, 0x59, 0x57, 0x57, 0x69, 0x61, 0x4e, 0x71, 0x55, 0x50, 0x48, 0x78, 0x4f, 0x47, 0x6d, 0x53, 0x64, 0x4e, 0x49, 0x64, 0x4d, 0x7a, 0x68, 0x6c, 0x70, 0x4f, 0x48, 0x6e, 0x6e, 0x76, 0x4c, 0x32, 0x4b, 0x70, 0x5a, 0x52, 0x75, 0x33, 0x51, 0x71, 0x43, 0x38, 0x32, 0x71, 0x72, 0x69, 0x61, 0x7a, 0x46, 0x4b, 0x30, 0x78, 0x63, 0x44, 0x79, 0x35, 0x31, 0x33, 0x5f, 0x42, 0x34, 0x56, 0x63, 0x73, 0x70, 0x66, 0x75, 0x66, 0x43, 0x71, 0x57, 0x72, 0x35, 0x5f, 0x49, 0x63, 0x6d, 0x32, 0x71, 0x4a, 0x6e, 0x62, 0x57, 0x36, 0x6d, 0x71, 0x67, 0x51, 0x35, 0x6f, 0x54, 0x55, 0x78, 0x20, 0x65, 0x79, 0x70, 0x74, 0x30, 0x51, 0x39, 0x51, 0x4e, 0x66, 0x35, 0x73, 0x77, 0x70, 0x51, 0x59, 0x78, 0x56, 0x52, 0x67, 0x39, 0x70, 0x33, 0x37, 0x52, 0x55, 0x4b, 0x6f, 0x62, 0x6e, 0x68, 0x6c, 0x78, 0x3a, 0x61, 0x73, 0x57, 0x68, 0x75, 0x63, 0x4a, 0x56, 0x33, 0x4b, 0x38, 0x70, 0x6d, 0x31, 0x54, 0x34, 0x58, 0x71, 0x4e, 0x6d, 0x74, 0x4b, 0x74, 0x4c, 0x45, 0x56, 0x65, 0x73, 0x75, 0x38, 0x4f, 0x61, 0x73, 0x64, 0x33, 0x4f, 0x5f, 0x39, 0x6a, 0x75, 0x64, 0x4a, 0x72, 0x75, 0x59, 0x64, 0x46, 0x55, 0x6e, 0x35, 0x6f, 0x5f, 0x44, 0x76, 0x58, 0x33, 0x41, 0x4e, 0x64, 0x72, 0x39, 0x67, 0x35, 0x78, 0x3f, 0x64, 0x4c, 0x78, 0x59, 0x63, 0x79, 0x6b, 0x33, 0x47, 0x66, 0x49, 0x79, 0x43, 0x34, 0x31, 0x58, 0x72, 0x34, 0x51, 0x41, 0x39, 0x75, 0x46, 0x4f, 0x75, 0x68, 0x78, 0x46, 0x55, 0x54, 0x4c, 0x72, 0x34, 0x51, 0x63, 0x58, 0x5f, 0x47, 0x75, 0x53, 0x39, 0x74, 0x64, 0x42, 0x65, 0x4e, 0x33, 0x4d, 0x6d, 0x58, 0x45, 0x45, 0x65, 0x69, 0x6b, 0x54, 0x73, 0x41, 0x32, 0x68, 0x58, 0x41, 0x45, 0x78, 0x2c, 0x57, 0x76, 0x55, 0x44, 0x6c, 0x67, 0x64, 0x65, 0x47, 0x33, 0x77, 0x37, 0x48, 0x46, 0x4c, 0x6e, 0x4d, 0x32, 0x51, 0x41, 0x39, 0x50, 0x30, 0x35, 0x30, 0x67, 0x6d, 0x63, 0x78, 0x41, 0x6c, 0x67, 0x35, 0x71, 0x58, 0x71, 0x4b, 0x45, 0x4f, 0x6a, 0x6d, 0x42, 0x56, 0x4d, 0x78, 0x2f, 0x63, 0x75, 0x65, 0x59, 0x45, 0x62, 0x69, 0x32, 0x42, 0x34, 0x30, 0x42, 0x6c, 0x79, 0x73, 0x55, 0x42, 0x4d, 0x7a, 0x33, 0x53, 0x54, 0x43, 0x42, 0x35, 0x34, 0x54, 0x33, 0x4b, 0x47, 0x50, 0x50, 0x5a, 0x53, 0x4d, 0x44, 0x57, 0x63, 0x58, 0x6c, 0x42, 0x59, 0x76, 0x49, 0x78, 0x61, 0x47, 0x72, 0x43, 0x43, 0x35, 0x49, 0x77, 0x4a, 0x75, 0x55, 0x4e, 0x33, 0x79, 0x73, 0x4f, 0x43, 0x30, 0x6d, 0x58, 0x48, 0x78, 0x64, 0x65, 0x38, 0x39, 0x55, 0x6b, 0x53, 0x6e, 0x50, 0x4f, 0x5a, 0x62, 0x4b, 0x76, 0x72, 0x77, 0x59, 0x68, 0x46, 0x6d, 0x39, 0x34, 0x77, 0x68, 0x50, 0x31, 0x7a, 0x75, 0x69, 0x38, 0x6d, 0x6c, 0x4d, 0x50, 0x6e, 0x74, 0x56, 0x69, 0x6a, 0x31, 0x50, 0x68, 0x6f, 0x61, 0x53, 0x4f, 0x49, 0x78, 0x71, 0x58, 0x48, 0x6b, 0x69, 0x38, 0x45, 0x35, 0x35, 0x38, 0x75, 0x61, 0x4a, 0x35, 0x71, 0x7a, 0x4e, 0x30, 0x76, 0x42, 0x71, 0x36, 0x65, 0x39, 0x48, 0x41, 0x7a, 0x47, 0x7a, 0x6a, 0x5f, 0x4a, 0x4d, 0x6f, 0x72, 0x4f, 0x68, 0x65, 0x52, 0x58, 0x33, 0x64, 0x61, 0x53, 0x30, 0x49, 0x61, 0x4d, 0x62, 0x5f, 0x58, 0x31, 0x63, 0x78, 0x27, 0x67, 0x49, 0x72, 0x72, 0x6b, 0x4c, 0x37, 0x70, 0x61, 0x4e, 0x41, 0x6e, 0x38, 0x37, 0x74, 0x73, 0x69, 0x5f, 0x37, 0x4f, 0x63, 0x53, 0x62, 0x64, 0x4f, 0x6e, 0x4b, 0x58, 0x66, 0x33, 0x5f, 0x36, 0x4a, 0x62, 0x4a, 0x7a, 0x73, 0x4c, 0x75, 0x78, 0x29, 0x63, 0x4f, 0x50, 0x48, 0x74, 0x6b, 0x56, 0x78, 0x5f, 0x41, 0x5f, 0x78, 0x6a, 0x56, 0x67, 0x61, 0x6a, 0x58, 0x47, 0x45, 0x43, 0x69, 0x51, 0x4c, 0x61, 0x6f, 0x57, 0x31, 0x41, 0x77, 0x6e, 0x4f, 0x57, 0x35, 0x68, 0x31, 0x45, 0x69, 0x34, 0x32, 0x56, 0x76, 0x4e, 0x7a, 0x75, 0x68, 0x61, 0x59, 0x56, 0x33, 0x38, 0x41, 0x70, 0x4e, 0x73, 0x46, 0x69, 0x56, 0x73, 0x47, 0x76, 0x30, 0x68, 0x74, 0x78, 0x44, 0x56, 0x6d, 0x6f, 0x68, 0x41, 0x35, 0x4e, 0x30, 0x4b, 0x36, 0x47, 0x59, 0x48, 0x38, 0x5a, 0x66, 0x4c, 0x6f, 0x62, 0x68, 0x5a, 0x4e, 0x4e, 0x4f, 0x6f, 0x41, 0x33, 0x4c, 0x78, 0x42, 0x76, 0x72, 0x50, 0x4e, 0x41, 0x42, 0x76, 0x49, 0x4b, 0x49, 0x57, 0x77, 0x57, 0x6c, 0x41, 0x49, 0x45, 0x79, 0x61, 0x73, 0x31, 0x59, 0x71, 0x58, 0x68, 0x61, 0x70, 0x57, 0x42, 0x62, 0x30, 0x32, 0x67, 0x39, 0x79, 0x70, 0x39, 0x65, 0x78, 0x1b, 0x76, 0x41, 0x35, 0x65, 0x4d, 0x62, 0x70, 0x65, 0x37, 0x57, 0x55, 0x78, 0x35, 0x4c, 0x6e, 0x54, 0x6f, 0x36, 0x33, 0x45, 0x6e, 0x56, 0x33, 0x75, 0x4c, 0x50, 0x74, 0x78, 0x31, 0x64, 0x63, 0x67, 0x38, 0x78, 0x33, 0x46, 0x35, 0x42, 0x71, 0x73, 0x69, 0x62, 0x71, 0x56, 0x31, 0x68, 0x41, 0x74, 0x71, 0x38, 0x74, 0x59, 0x43, 0x69, 0x71, 0x4e, 0x7a, 0x56, 0x71, 0x70, 0x67, 0x56, 0x4c, 0x71, 0x49, 0x66, 0x6b, 0x36, 0x42, 0x34, 0x31, 0x71, 0x4d, 0x55, 0x70, 0x5f, 0x74, 0x33, 0x65, 0x73, 0x6a, 0x6e, 0x62, 0x35, 0x78, 0x54, 0x55, 0x68, 0x39, 0x50, 0x31, 0x63, 0x78, 0x77, 0x58, 0x6f, 0x44, 0x43, 0x6f, 0x32, 0x6c, 0x5a, 0x78, 0x53, 0x53, 0x73, 0x46, 0x4e, 0x6b, 0x34, 0x6f, 0x39, 0x46, 0x74, 0x38, 0x42, 0x75, 0x4f, 0x75, 0x4e, 0x78, 0x6b, 0x35, 0x65, 0x42, 0x52, 0x35, 0x4b, 0x32, 0x6a, 0x76, 0x4f, 0x74, 0x4c, 0x57, 0x48, 0x34, 0x4d, 0x34, 0x71, 0x57, 0x43, 0x45, 0x5a, 0x4f, 0x62, 0x6e, 0x48, 0x39, 0x5f, 0x68, 0x42, 0x34, 0x79, 0x70, 0x4a, 0x4d, 0x66, 0x7a, 0x5a, 0x58, 0x6e, 0x51, 0x41, 0x34, 0x68, 0x4b, 0x6b, 0x62, 0x54, 0x78, 0x1a, 0x57, 0x44, 0x70, 0x6a, 0x78, 0x6e, 0x61, 0x57, 0x6f, 0x73, 0x6e, 0x4a, 0x49, 0x56, 0x35, 0x36, 0x4b, 0x76, 0x6b, 0x64, 0x62, 0x51, 0x76, 0x68, 0x76, 0x67, 0xa5, 0x61, 0x65, 0x63, 0x69, 0x50, 0x41, 0x61, 0x54, 0x67, 0x68, 0x6f, 0x69, 0x58, 0x4c, 0x4c, 0x4a, 0x61, 0x48, 0x65, 0x63, 0x55, 0x43, 0x37, 0x62, 0x67, 0x74, 0x46, 0x39, 0x79, 0x79, 0x4b, 0x6b, 0x66, 0x4e, 0x4d, 0x76, 0x4c, 0x30, 0x74, 0x64, 0x62, 0x58, 0x4a, 0x53, 0xa1, 0x61, 0x4e, 0x64, 0x71, 0x43, 0x62, 0x6d, 0x63, 0x55, 0x31, 0x32, 0x78, 0x3f, 0x50, 0x48, 0x51, 0x76, 0x6d, 0x69, 0x31, 0x71, 0x4a, 0x45, 0x65, 0x71, 0x75, 0x44, 0x58, 0x6d, 0x6f, 0x6f, 0x46, 0x54, 0x51, 0x76, 0x58, 0x6c, 0x49, 0x4d, 0x62, 0x54, 0x4d, 0x55, 0x78, 0x4f, 0x38, 0x49, 0x35, 0x34, 0x5a, 0x77, 0x67, 0x6d, 0x59, 0x36, 0x48, 0x50, 0x6d, 0x72, 0x32, 0x7a, 0x5f, 0x66, 0x77, 0x38, 0x76, 0x47, 0x4a, 0x31, 0x33, 0x70, 0x35, 0x6e, 0x34, 0x35, 0x4c, 0x6f, 0x53, 0x6d, 0x53, 0x67, 0x66, 0x37, 0x4e, 0x48, 0x66, 0x75, 0x57, 0x70, 0x77, 0x38, 0x77, 0x70, 0x49, 0x58, 0x4b, 0x38, 0x6e, 0x79, 0x33, 0x4a, 0x66, 0x4f, 0x44, 0x55, 0x42, 0x41, 0x39, 0x57, 0x6e, 0x71, 0x63, 0x43, 0x43, 0x37, 0x5f, 0x73, 0x31, 0x45, 0x36, 0x30, 0x45, 0x33, 0x64, 0x78, 0x1b, 0x68, 0x64, 0x43, 0x62, 0x5f, 0x65, 0x69, 0x6d, 0x69, 0x53, 0x56, 0x62, 0x67, 0x78, 0x75, 0x6c, 0x59, 0x33, 0x5f, 0x4c, 0x38, 0x44, 0x56, 0x5f, 0x6f, 0x6d, 0x67, 0x61, 0x42, 0x78, 0x3c, 0x68, 0x4a, 0x72, 0x7a, 0x31, 0x67, 0x56, 0x4c, 0x4e, 0x35, 0x36, 0x41, 0x49, 0x4c, 0x64, 0x66, 0x33, 0x6c, 0x4e, 0x33, 0x59, 0x58, 0x38, 0x59, 0x34, 0x44, 0x65, 0x47, 0x4c, 0x78, 0x44, 0x71, 0x55, 0x72, 0x37, 0x41, 0x41, 0x52, 0x44, 0x32, 0x70, 0x79, 0x50, 0x57, 0x7a, 0x58, 0x49, 0x76, 0x4e, 0x65, 0x4a, 0x33, 0x76, 0x4e, 0x45, 0x35, 0x4f, 0x4e, 0x67, 0x61, 0x78, 0x1d, 0x39, 0x61, 0x62, 0x57, 0x5f, 0x79, 0x76, 0x7a, 0x6b, 0x53, 0x38, 0x4c, 0x73, 0x73, 0x43, 0x5a, 0x6c, 0x66, 0x6e, 0x38, 0x58, 0x53, 0x44, 0x57, 0x37, 0x78, 0x66, 0x6c, 0x39, 0x78, 0x2d, 0x59, 0x46, 0x59, 0x5f, 0x4a, 0x76, 0x41, 0x73, 0x4d, 0x50, 0x65, 0x52, 0x4c, 0x39, 0x54, 0x42, 0x39, 0x72, 0x51, 0x72, 0x48, 0x34, 0x52, 0x44, 0x69, 0x65, 0x79, 0x63, 0x76, 0x36, 0x33, 0x39, 0x4e, 0x4e, 0x71, 0x67, 0x5a, 0x54, 0x31, 0x38, 0x57, 0x4d, 0x4e, 0x72, 0x33, 0x78, 0x18, 0x71, 0x79, 0x33, 0x69, 0x73, 0x68, 0x44, 0x4e, 0x63, 0x50, 0x49, 0x68, 0x75, 0x56, 0x41, 0x46, 0x41, 0x6a, 0x32, 0x70, 0x76, 0x46, 0x76, 0x44, 0x78, 0x51, 0x78, 0x55, 0x30, 0x6e, 0x4c, 0x65, 0x4d, 0x72, 0x39, 0x66, 0x4e, 0x46, 0x6b, 0x64, 0x6b, 0x7a, 0x65, 0x70, 0x69, 0x41, 0x50, 0x68, 0x43, 0x46, 0x77, 0x6b, 0x68, 0x46, 0x72, 0x5a, 0x48, 0x37, 0x57, 0x69, 0x47, 0x65, 0x49, 0x6d, 0x37, 0x6e, 0x7a, 0x32, 0x34, 0x64, 0x57, 0x42, 0x30, 0x36, 0x63, 0x4d, 0x59, 0x54, 0x48, 0x65, 0x34, 0x48, 0x70, 0x59, 0x56, 0x59, 0x64, 0x45, 0x61, 0x64, 0x30, 0x4f, 0x6d, 0x4e, 0x33, 0x79, 0x6c, 0x34, 0x70, 0x51, 0x73, 0x54, 0x6a, 0x44, 0x6f, 0x42, 0x69, 0x78, 0x20, 0x63, 0x44, 0x6c, 0x79, 0x32, 0x6b, 0x7a, 0x78, 0x4e, 0x62, 0x41, 0x51, 0x4e, 0x4c, 0x31, 0x57, 0x77, 0x62, 0x73, 0x6a, 0x45, 0x76, 0x74, 0x31, 0x36, 0x4d, 0x62, 0x30, 0x58, 0x41, 0x56, 0x51, 0xa5, 0x61, 0x34, 0x66, 0x36, 0x63, 0x64, 0x49, 0x6d, 0x4d, 0x69, 0x4f, 0x6d, 0x4e, 0x42, 0x35, 0x65, 0x58, 0x69, 0x39, 0x69, 0x79, 0x30, 0x74, 0x30, 0x50, 0x45, 0x42, 0x49, 0x77, 0x69, 0x57, 0x30, 0x6a, 0x51, 0x72, 0x63, 0x6b, 0x53, 0x42, 0x6a, 0x6d, 0x74, 0x55, 0x37, 0x78, 0x76, 0x4b, 0x59, 0x66, 0x45, 0x62, 0x48, 0x72, 0x63, 0x32, 0x49, 0x79, 0x69, 0x71, 0x6f, 0x39, 0x70, 0x30, 0x4e, 0x7a, 0x46, 0x70, 0x69, 0x39, 0x69, 0x53, 0x52, 0x33, 0x4d, 0x57, 0x32, 0x4b, 0x72, 0x53, 0x46, 0x63, 0x6c, 0x44, 0x50, 0x57, 0x5f, 0x5a, 0x65, 0x6c, 0x4f, 0x31, 0x75, 0x6f, 0x5a, 0x7a, 0x4a, 0x78, 0x1d, 0x58, 0x6e, 0x4a, 0x38, 0x4c, 0x6e, 0x79, 0x56, 0x39, 0x4d, 0x32, 0x6d, 0x32, 0x5a, 0x59, 0x58, 0x33, 0x30, 0x43, 0x75, 0x41, 0x52, 0x44, 0x79, 0x6b, 0x51, 0x79, 0x56, 0x72, 0x71, 0x44, 0x7a, 0x32, 0x6b, 0x4a, 0x6d, 0x59, 0x44, 0x4b, 0x68, 0x4b, 0x37, 0x61, 0x6d, 0x67, 0x42, 0x50, 0x78, 0x2f, 0x50, 0x79, 0x34, 0x56, 0x67, 0x31, 0x45, 0x53, 0x55, 0x76, 0x73, 0x4b, 0x79, 0x44, 0x36, 0x6d, 0x44, 0x31, 0x30, 0x48, 0x49, 0x67, 0x77, 0x49, 0x38, 0x58, 0x49, 0x65, 0x52, 0x55, 0x69, 0x74, 0x70, 0x5f, 0x32, 0x68, 0x68, 0x74, 0x4f, 0x7a, 0x48, 0x36, 0x53, 0x7a, 0x77, 0x34, 0x39, 0x6c, 0x41, 0x33, 0x6d, 0x45, 0x66, 0x4d, 0x4b, 0x41, 0x37, 0x4c, 0x79, 0x6e, 0x78, 0x28, 0x69, 0x6a, 0x53, 0x66, 0x4c, 0x42, 0x4c, 0x62, 0x6e, 0x41, 0x58, 0x44, 0x5a, 0x68, 0x32, 0x51, 0x6c, 0x77, 0x52, 0x37, 0x38, 0x6d, 0x6b, 0x35, 0x35, 0x51, 0x36, 0x37, 0x65, 0x46, 0x55, 0x57, 0x77, 0x70, 0x77, 0x78, 0x61, 0x75, 0x55, 0x4d, 0x73, 0x34, 0x78, 0x73, 0x65, 0x71, 0x61, 0x66, 0x50, 0x32, 0x47, 0x48, 0x4f, 0x54, 0x70, 0x43, 0x4a, 0x64, 0x56, 0x35, 0x77, 0x6e, 0x79, 0x53, 0x42, 0x6d, 0x63, 0x52, 0x72, 0x41, 0x61, 0x68, 0x45, 0x39, 0x31, 0x32, 0x4e, 0x58, 0x6a, 0x64, 0x65, 0x79, 0x53, 0x73, 0x61, 0x48, 0x78, 0x2b, 0x43, 0x4f, 0x71, 0x64, 0x54, 0x48, 0x57, 0x45, 0x4a, 0x56, 0x51, 0x47, 0x41, 0x69, 0x72, 0x4d, 0x62, 0x69, 0x49, 0x50, 0x35, 0x30, 0x66, 0x67, 0x48, 0x32, 0x44, 0x71, 0x5f, 0x77, 0x59, 0x73, 0x77, 0x51, 0x6f, 0x4b, 0x79, 0x6d, 0x78, 0x5f, 0x65, 0x6d, 0x43, 0x6c, 0x36, 0x36, 0x6b, 0x77, 0x68, 0x66, 0x4f, 0x49, 0x6e, 0x56, 0x33, 0x31, 0xa6, 0x61, 0x31, 0x66, 0x51, 0x75, 0x77, 0x75, 0x53, 0x6b, 0x65, 0x67, 0x71, 0x4f, 0x66, 0x6d, 0x6c, 0x6d, 0x57, 0x6c, 0x4c, 0x6a, 0x72, 0x35, 0x55, 0x67, 0x41, 0x75, 0x33, 0x65, 0x63, 0x44, 0x30, 0x72, 0x78, 0xa1, 0x61, 0x48, 0x64, 0x58, 0x64, 0x61, 0x69, 0x65, 0x65, 0x42, 0x52, 0x49, 0x4d, 0x6d, 0x69, 0x45, 0x56, 0x33, 0x53, 0x49, 0x37, 0x4d, 0x6f, 0x68, 0x6f, 0x55, 0x47, 0x64, 0x6c, 0x4d, 0x73, 0x4d, 0x68, 0x41, 0x67, 0x41, 0x33, 0x7a, 0x54, 0x6f, 0x34, 0x63, 0x30, 0x6f, 0x36, 0x64, 0x62, 0x76, 0x72, 0x62, 0x6c, 0x76, 0x54, 0x55, 0x4a, 0x4c, 0x54, 0x49, 0x51, 0x39, 0x50, 0x4f, 0x46, 0x78, 0x3d, 0x32, 0x6e, 0x5f, 0x33, 0x47, 0x55, 0x4b, 0x76, 0x35, 0x39, 0x49, 0x42, 0x5a, 0x47, 0x58, 0x4c, 0x70, 0x73, 0x4a, 0x56, 0x42, 0x35, 0x76, 0x39, 0x49, 0x6d, 0x7a, 0x50, 0x52, 0x69, 0x71, 0x44, 0x68, 0x4c, 0x55, 0x54, 0x50, 0x39, 0x68, 0x54, 0x6a, 0x75, 0x66, 0x52, 0x44, 0x35, 0x32, 0x4c, 0x55, 0x5a, 0x51, 0x46, 0x34, 0x43, 0x5f, 0x6b, 0x6e, 0x51, 0x5a, 0x69, 0x50, 0x78, 0x1f, 0x39, 0x7a, 0x4b, 0x44, 0x49, 0x4f, 0x48, 0x63, 0x48, 0x7a, 0x51, 0x4b, 0x55, 0x44, 0x30, 0x62, 0x4a, 0x76, 0x66, 0x32, 0x6e, 0x65, 0x58, 0x63, 0x7a, 0x42, 0x66, 0x41, 0x4e, 0x35, 0x67, 0xa4, 0x61, 0x70, 0x60, 0x65, 0x59, 0x44, 0x33, 0x59, 0x35, 0xa1, 0x61, 0x47, 0x64, 0x67, 0x62, 0x42, 0x4c, 0x64, 0x49, 0x66, 0x61, 0x66, 0xa1, 0x61, 0x4a, 0x64, 0x33, 0x56, 0x63, 0x63, 0x61, 0x6a, 0xa1, 0x61, 0x67, 0x61, 0x78, 0x78, 0x36, 0x61, 0x63, 0x73, 0x36, 0x68, 0x30, 0x52, 0x63, 0x57, 0x53, 0x75, 0x65, 0x53, 0x46, 0x46, 0x6b, 0x42, 0x74, 0x36, 0x6c, 0x4f, 0x6f, 0x47, 0x67, 0x79, 0x36, 0x42, 0x67, 0x64, 0x6d, 0x6d, 0x77, 0x38, 0x34, 0x65, 0x69, 0x30, 0x73, 0x55, 0x43, 0x4c, 0x59, 0x50, 0x67, 0x30, 0x6e, 0x42, 0x71, 0x35, 0x39, 0x33, 0x66, 0x72, 0x36, 0x78, 0x38, 0x4c, 0x6b, 0x54, 0x49, 0x33, 0x31, 0x4e, 0x48, 0x64, 0x75, 0x55, 0x46, 0x4c, 0x6f, 0x56, 0x71, 0x51, 0x30, 0x4e, 0x30, 0x47, 0x58, 0x6d, 0x43, 0x6e, 0x51, 0x39, 0x61, 0x4e, 0x54, 0x5a, 0x5a, 0x69, 0x66, 0x6c, 0x53, 0x4c, 0x4b, 0x7a, 0x50, 0x71, 0x69, 0x4f, 0x5a, 0x75, 0x4a, 0x64, 0x64, 0x6c, 0x69, 0x50, 0x71, 0x6e, 0x42, 0x78, 0x54, 0x75, 0x4e, 0x6e, 0x32, 0x39, 0x6e, 0x36, 0x75, 0x64, 0x39, 0x67, 0x33, 0x71, 0x7a, 0x78, 0x5a, 0x45, 0x72, 0x38, 0x68, 0x58, 0x74, 0x78, 0x1e, 0x32, 0x38, 0x30, 0x73, 0x6b, 0x4d, 0x6e, 0x36, 0x47, 0x38, 0x68, 0x55, 0x52, 0x57, 0x75, 0x57, 0x69, 0x61, 0x6e, 0x6a, 0x4c, 0x31, 0x68, 0x6d, 0x75, 0x6d, 0x36, 0x47, 0x72, 0x44, 0x77, 0x31, 0x65, 0x30, 0x42, 0x39, 0x34, 0x51, 0x5a, 0x4d, 0x30, 0x73, 0x42, 0x77, 0x49, 0x6d, 0x52, 0x70, 0x5f, 0x41, 0x65, 0x76, 0x39, 0x42, 0x78, 0x39, 0x38, 0x67, 0x64, 0x42, 0x5a, 0x4e, 0x37, 0x4b, 0x63, 0x55, 0x41, 0x45, 0x52, 0x30, 0x4b, 0x52, 0x70, 0x30, 0x6c, 0x51, 0x78, 0x6d, 0x33, 0x76, 0x64, 0x4f, 0x64, 0x34, 0x6d, 0x30, 0x77, 0x4e, 0x70, 0x44, 0x42, 0x4d, 0x70, 0x34, 0x66, 0x6c, 0x36, 0x6b, 0x50, 0x50, 0x52, 0x69, 0x70, 0x6b, 0x5a, 0x6f, 0x4c, 0x4a, 0x43, 0x62, 0x32, 0x32, 0x6b, 0x78, 0x1c, 0x4b, 0x31, 0x58, 0x76, 0x56, 0x6c, 0x57, 0x35, 0x71, 0x57, 0x6c, 0x65, 0x53, 0x58, 0x53, 0x34, 0x59, 0x6b, 0x64, 0x38, 0x78, 0x6c, 0x6d, 0x34, 0x4e, 0x51, 0x6c, 0x67, 0x78, 0x1f, 0x77, 0x72, 0x57, 0x56, 0x33, 0x72, 0x4a, 0x73, 0x6f, 0x45, 0x70, 0x78, 0x71, 0x6d, 0x56, 0x52, 0x4d, 0x73, 0x54, 0x34, 0x4a, 0x31, 0x77, 0x43, 0x38, 0x77, 0x75, 0x55, 0x64, 0x38, 0x47, 0x6e, 0x44, 0x55, 0x65, 0x68, 0x37, 0x58, 0x68, 0x36, 0x63, 0x43, 0x50, 0x79, 0x45, 0x78, 0xa8, 0x61, 0x30, 0x6d, 0x6e, 0x48, 0x33, 0x68, 0x52, 0x61, 0x52, 0x79, 0x65, 0x70, 0x79, 0x31, 0x4e, 0x61, 0x33, 0x6c, 0x41, 0x51, 0x70, 0x31, 0x64, 0x47, 0x4e, 0x63, 0x48, 0x31, 0x4f, 0x52, 0x63, 0x56, 0x55, 0x6e, 0x6d, 0x79, 0x6f, 0x72, 0x57, 0x31, 0x41, 0x55, 0x73, 0x74, 0x72, 0x63, 0x73, 0x43, 0x65, 0x5f, 0x79, 0x70, 0x67, 0x51, 0x6b, 0x68, 0x63, 0x69, 0x73, 0x69, 0x73, 0x31, 0x61, 0x68, 0x7a, 0x51, 0x61, 0x6d, 0x64, 0x37, 0x41, 0x4c, 0x64, 0x63, 0x79, 0x59, 0x38, 0x62, 0x46, 0x4e, 0x61, 0x41, 0x68, 0x54, 0x4e, 0x43, 0x76, 0x63, 0x73, 0x61, 0x36, 0x61, 0x77, 0x69, 0x4f, 0x69, 0x6d, 0x57, 0x42, 0x50, 0x38, 0x54, 0x52, 0x6b, 0x48, 0x66, 0x73, 0x64, 0x66, 0x67, 0x53, 0x71, 0x79, 0x46, 0x71, 0x78, 0x66, 0x6e, 0x50, 0x78, 0x71, 0x64, 0x43, 0x32, 0x36, 0x55, 0x31, 0x62, 0x4a, 0x4b, 0x6b, 0x61, 0x64, 0x4f, 0x30, 0x71, 0x59, 0x63, 0x33, 0x42, 0x76, 0x4f, 0x63, 0x4f, 0x55, 0x76, 0x4c, 0x42, 0x52, 0x52, 0x32, 0x53, 0x6f, 0x71, 0x74, 0x46, 0x5f, 0x75, 0x49, 0x44, 0x43, 0x31, 0x47, 0x49, 0x72, 0x6d, 0x4a, 0x56, 0x41, 0x4c, 0x42, 0x75, 0x6a, 0x55, 0x37, 0x5a, 0x6d, 0x59, 0x37, 0x5f, 0x52, 0x37, 0x70, 0x4f, 0x39, 0x52, 0x30, 0x7a, 0x6b, 0x72, 0x76, 0x57, 0x46, 0x38, 0x67, 0x5f, 0x34, 0x6c, 0x4a, 0x69, 0x51, 0x4a, 0x4a, 0x4a, 0x48, 0x6e, 0x47, 0x6a, 0x55, 0x79, 0x38, 0x33, 0x6b, 0x52, 0x71, 0x55, 0x67, 0x45, 0x5a, 0x68, 0x35, 0x4e, 0x4b, 0x39, 0x4f, 0x71, 0x37, 0x73, 0x78, 0x27, 0x4f, 0x39, 0x31, 0x71, 0x34, 0x4e, 0x4e, 0x48, 0x61, 0x71, 0x43, 0x66, 0x45, 0x79, 0x47, 0x38, 0x39, 0x4f, 0x62, 0x43, 0x71, 0x56, 0x51, 0x4b, 0x38, 0x42, 0x69, 0x75, 0x36, 0x51, 0x65, 0x4d, 0x58, 0x37, 0x68, 0x41, 0x46, 0x63, 0x38, 0x78, 0x38, 0x42, 0x6d, 0x49, 0x6b, 0x47, 0x67, 0x43, 0x32, 0x31, 0x57, 0x66, 0x66, 0x31, 0x4a, 0x79, 0x37, 0x61, 0x50, 0x44, 0x63, 0x36, 0x45, 0x75, 0x4d, 0x34, 0x78, 0x79, 0x49, 0x42, 0x49, 0x6d, 0x79, 0x6a, 0x6f, 0x45, 0x6f, 0x76, 0x57, 0x4a, 0x64, 0x31, 0x4b, 0x45, 0x71, 0x76, 0x6f, 0x72, 0x44, 0x6f, 0x78, 0x47, 0x71, 0x54, 0x6d, 0x46, 0x39, 0xa5, 0x61, 0x5a, 0x64, 0x32, 0x32, 0x46, 0x58, 0x61, 0x5f, 0x6e, 0x6f, 0x7a, 0x73, 0x4d, 0x42, 0x74, 0x6c, 0x30, 0x45, 0x6a, 0x46, 0x73, 0x50, 0x48, 0x62, 0x59, 0x6a, 0x63, 0x32, 0x32, 0x78, 0x61, 0x53, 0xa1, 0x61, 0x42, 0x64, 0x62, 0x4a, 0x33, 0x45, 0x6a, 0x77, 0x69, 0x45, 0x6c, 0x48, 0x4f, 0x43, 0x74, 0x71, 0x72, 0x69, 0x50, 0x49, 0x55, 0x45, 0x48, 0x30, 0x62, 0x68, 0x6d, 0x78, 0x24, 0x67, 0x75, 0x42, 0x47, 0x63, 0x76, 0x32, 0x69, 0x34, 0x42, 0x43, 0x39, 0x7a, 0x50, 0x72, 0x58, 0x4c, 0x34, 0x51, 0x66, 0x77, 0x71, 0x73, 0x4d, 0x63, 0x79, 0x48, 0x62, 0x64, 0x4a, 0x42, 0x68, 0x6c, 0x57, 0x57, 0x57, 0x78, 0x54, 0x61, 0x50, 0x77, 0x6a, 0x39, 0x67, 0x50, 0x73, 0x71, 0x4d, 0x61, 0x56, 0x66, 0x4f, 0x4c, 0x75, 0x6e, 0x7a, 0x6d, 0x53, 0x68, 0x36, 0x52, 0x53, 0x53, 0x4b, 0x39, 0x6b, 0x54, 0x50, 0x72, 0x66, 0x50, 0x70, 0x64, 0x45, 0x76, 0x6c, 0x53, 0x69, 0x30, 0x72, 0x78, 0x39, 0x75, 0x6a, 0x41, 0x69, 0x38, 0x79, 0x6b, 0x47, 0x49, 0x43, 0x49, 0x78, 0x75, 0x46, 0x5f, 0x47, 0x59, 0x42, 0x32, 0x6b, 0x30, 0x77, 0x6c, 0x59, 0x35, 0x53, 0x46, 0x76, 0x4f, 0x69, 0x41, 0x47, 0x69, 0x33, 0x45, 0x53, 0x6d, 0x6c, 0x69, 0x72, 0x6d, 0x47, 0x78, 0x58, 0x6c, 0x64, 0x77, 0x5f, 0x70, 0x33, 0x7a, 0x4e, 0x61, 0x4b, 0x78, 0x1a, 0x38, 0x77, 0x71, 0x70, 0x68, 0x63, 0x70, 0x47, 0x43, 0x74, 0x73, 0x62, 0x66, 0x59, 0x5f, 0x45, 0x46, 0x31, 0x4d, 0x64, 0x4f, 0x32, 0x65, 0x6b, 0x31, 0x62, 0x76, 0x43, 0x7a, 0x70, 0x33, 0x33, 0x50, 0x6f, 0x58, 0x4d, 0x67, 0x33, 0x53, 0x72, 0x55, 0x59, 0x72, 0x52, 0x69, 0x58, 0x5a, 0x5a, 0x51, 0x78, 0x27, 0x53, 0x67, 0x61, 0x4b, 0x77, 0x75, 0x32, 0x32, 0x55, 0x5f, 0x4b, 0x4c, 0x48, 0x66, 0x4f, 0x48, 0x50, 0x51, 0x71, 0x71, 0x69, 0x69, 0x67, 0x6b, 0x6d, 0x66, 0x69, 0x31, 0x43, 0x41, 0x6e, 0x78, 0x6d, 0x47, 0x57, 0x46, 0x46, 0x62, 0x6e, 0x78, 0x26, 0x4d, 0x4b, 0x34, 0x6a, 0x50, 0x59, 0x44, 0x5f, 0x59, 0x6c, 0x4a, 0x32, 0x4c, 0x4f, 0x6a, 0x76, 0x4e, 0x67, 0x37, 0x30, 0x31, 0x6d, 0x67, 0x65, 0x38, 0x46, 0x39, 0x50, 0x77, 0x79, 0x5a, 0x48, 0x65, 0x77, 0x72, 0x4f, 0x78, 0x44, 0xa3, 0x61, 0x4a, 0x65, 0x51, 0x67, 0x56, 0x47, 0x36, 0x63, 0x50, 0x58, 0x5a, 0x6a, 0x6d, 0x79, 0x5a, 0x51, 0x6d, 0x53, 0x77, 0x48, 0x64, 0x54, 0x62, 0x44, 0x79, 0xa1, 0x61, 0x59, 0x6b, 0x68, 0x71, 0x34, 0x36, 0x50, 0x4b, 0x6a, 0x53, 0x46, 0x4f, 0x32, 0x70, 0x49, 0x55, 0x74, 0x53, 0x50, 0x31, 0x6b, 0x6f, 0x6a, 0x5a, 0x4c, 0x4e, 0x34, 0x43, 0x6a, 0x63, 0x78, 0x26, 0x4f, 0x70, 0x58, 0x32, 0x73, 0x6e, 0x34, 0x67, 0x6b, 0x57, 0x66, 0x71, 0x50, 0x51, 0x67, 0x4a, 0x64, 0x45, 0x56, 0x31, 0x6b, 0x48, 0x53, 0x65, 0x68, 0x55, 0x73, 0x59, 0x70, 0x56, 0x4c, 0x45, 0x6a, 0x59, 0x38, 0x39, 0x73, 0x46, 0x78, 0x1a, 0x58, 0x53, 0x70, 0x6a, 0x54, 0x73, 0x56, 0x42, 0x76, 0x55, 0x37, 0x56, 0x35, 0x51, 0x7a, 0x68, 0x34, 0x4b, 0x74, 0x51, 0x39, 0x75, 0x67, 0x6d, 0x41, 0x65, 0xa3, 0x61, 0x39, 0xa1, 0x61, 0x6b, 0x69, 0x4c, 0x66, 0x4d, 0x52, 0x69, 0x78, 0x4c, 0x4e, 0x6e, 0x61, 0x49, 0x70, 0x44, 0x51, 0x7a, 0x67, 0x79, 0x49, 0x47, 0x63, 0x44, 0x64, 0x74, 0x51, 0x75, 0x7a, 0x33, 0x77, 0x61, 0x56, 0x66, 0x56, 0x6f, 0x4d, 0x59, 0x61, 0x76, 0x66, 0x48, 0x72, 0x43, 0x42, 0x47, 0x55, 0x78, 0x2b, 0x54, 0x4c, 0x4e, 0x73, 0x73, 0x51, 0x61, 0x76, 0x72, 0x48, 0x4a, 0x57, 0x79, 0x30, 0x36, 0x57, 0x61, 0x44, 0x59, 0x50, 0x6f, 0x51, 0x72, 0x78, 0x6c, 0x78, 0x62, 0x56, 0x6c, 0x56, 0x6b, 0x35, 0x70, 0x6e, 0x30, 0x39, 0x4b, 0x50, 0x68, 0x78, 0x35, 0x70, 0x51, 0x78, 0x2d, 0x77, 0x70, 0x43, 0x39, 0x4e, 0x55, 0x5f, 0x51, 0x69, 0x39, 0x41, 0x6c, 0x4f, 0x58, 0x49, 0x7a, 0x4a, 0x46, 0x7a, 0x43, 0x47, 0x5f, 0x64, 0x64, 0x73, 0x66, 0x62, 0x39, 0x46, 0x53, 0x45, 0x55, 0x34, 0x4e, 0x54, 0x37, 0x4d, 0x6a, 0x64, 0x7a, 0x47, 0x6f, 0x43, 0x6c, 0x31, 0x78, 0x34, 0x4c, 0x6c, 0x6b, 0x47, 0x56, 0x33, 0x7a, 0x43, 0x50, 0x73, 0x70, 0x4c, 0x63, 0x31, 0x5a, 0x38, 0x55, 0x47, 0x4d, 0x76, 0x4d, 0x35, 0x55, 0x32, 0x65, 0x4d, 0x6d, 0x33, 0x52, 0x74, 0x68, 0x61, 0x4c, 0x69, 0x71, 0x4e, 0x59, 0x45, 0x64, 0x70, 0x5a, 0x39, 0x42, 0x6e, 0x55, 0x6d, 0x47, 0x66, 0x77, 0x6d, 0x58, 0x4d, 0x78, 0x19, 0x34, 0x74, 0x41, 0x57, 0x64, 0x36, 0x41, 0x70, 0x77, 0x39, 0x6b, 0x4f, 0x50, 0x31, 0x38, 0x33, 0x48, 0x70, 0x79, 0x68, 0x43, 0x6b, 0x77, 0x6a, 0x77, 0xa2, 0x61, 0x63, 0x66, 0x70, 0x4a, 0x42, 0x32, 0x53, 0x54, 0x68, 0x72, 0x6a, 0x5f, 0x51, 0x58, 0x56, 0x4e, 0x79, 0x67, 0x7a, 0x36, 0x71, 0x4e, 0x31, 0x77, 0x54, 0x6c, 0x34, 0x48, 0x65, 0x5f, 0x59, 0x6d, 0x66, 0x42, 0x62, 0x45, 0x6f, 0x65, 0x78, 0x68, 0x67, 0x68, 0x70, 0x31, 0x33, 0x71, 0x49, 0x65, 0x43, 0x4c, 0x42, 0x62, 0x59, 0x77, 0x74, 0x63, 0x53, 0x32, 0x71, 0x7a, 0x32, 0x79, 0x72, 0x33, 0x38, 0x4d, 0x64, 0x49, 0x73, 0x44, 0x4d, 0x42, 0x70, 0x32, 0x5f, 0x71, 0x46, 0x6a, 0x79, 0x61, 0x4f, 0x77, 0x41, 0x75, 0x4a, 0x67, 0x6e, 0x54, 0x61, 0x54, 0x53, 0x4d, 0x70, 0x52, 0x67, 0x6c, 0x7a, 0x35, 0x73, 0x35, 0x53, 0x78, 0x6a, 0x4c, 0x78, 0x34, 0x6e, 0x51, 0x34, 0x69, 0x6e, 0x58, 0x62, 0x59, 0x68, 0x5f, 0x34, 0x56, 0x64, 0x4e, 0x39, 0x42, 0x54, 0x41, 0x6b, 0x4a, 0x5a, 0x41, 0x63, 0x4a, 0x66, 0x54, 0x74, 0x53, 0x35, 0x48, 0x37, 0x30, 0x58, 0x7a, 0x33, 0x4c, 0x6c, 0x33, 0x78, 0x1d, 0x54, 0x63, 0x46, 0x6f, 0x7a, 0x6f, 0x5f, 0x62, 0x30, 0x30, 0x67, 0x6a, 0x57, 0x74, 0x46, 0x4c, 0x65, 0x47, 0x75, 0x76, 0x38, 0x78, 0x32, 0x63, 0x52, 0x4c, 0x65, 0x49, 0x5a, 0xa2, 0x61, 0x44, 0x71, 0x43, 0x61, 0x6e, 0x44, 0x66, 0x4e, 0x67, 0x4f, 0x65, 0x54, 0x6f, 0x70, 0x62, 0x6f, 0x57, 0x37, 0x33, 0x64, 0x6e, 0x35, 0x47, 0x72, 0x6f, 0x48, 0x36, 0x37, 0x5f, 0x6e, 0x52, 0x66, 0x4a, 0x32, 0x43, 0x58, 0x58, 0x64, 0x58, 0x4f, 0x6f, 0x36, 0x6c, 0x64, 0x47, 0x42, 0x50, 0x58, 0x6f, 0x66, 0x5a, 0x79, 0x32, 0x43, 0x64, 0x53, 0x78, 0x61, 0x73, 0x65, 0x4f, 0x74, 0x32, 0x36, 0x45, 0x34, 0x33, 0x73, 0x75, 0x35, 0x62, 0x4c, 0x7a, 0x39, 0x50, 0x50, 0x78, 0x58, 0x51, 0x79, 0x57, 0x36, 0x56, 0x5a, 0x48, 0x5f, 0x36, 0x4d, 0x68, 0x72, 0x67, 0x58, 0x47, 0x67, 0x74, 0x30, 0x52, 0x79, 0x30, 0x39, 0x66, 0x77, 0x52, 0x45, 0x6d, 0x50, 0x6e, 0x6a, 0x6b, 0x5a, 0x74, 0x33, 0x54, 0x5a, 0x5f, 0x4b, 0x48, 0x6f, 0x59, 0x55, 0x35, 0x45, 0x76, 0x30, 0x46, 0x34, 0x75, 0x79, 0x4f, 0x70, 0x32, 0x34, 0x4d, 0x73, 0x6e, 0x54, 0x4f, 0x47, 0x39, 0x62, 0x49, 0x74, 0x77, 0x4e, 0x59, 0x49, 0x61, 0x71, 0x57, 0x62, 0x32, 0x32, 0x53, 0x50, 0x50, 0x77, 0x32, 0x64, 0x42, 0x71, 0x35, 0x39, 0x39, 0x4c, 0x54, 0x45, 0x6a, 0x4d, 0x77, 0x71, 0x6e, 0x5f, 0x62, 0x56, 0x44, 0x6e, 0x73, 0x62, 0x6c, 0xa6, 0x61, 0x6d, 0x65, 0x33, 0x79, 0x6b, 0x76, 0x4f, 0x62, 0x54, 0x61, 0x63, 0x4f, 0x35, 0x5f, 0x67, 0x4e, 0x43, 0x76, 0x37, 0x79, 0x79, 0x50, 0x66, 0x74, 0x79, 0x5f, 0x4d, 0x73, 0x4f, 0x66, 0x71, 0x58, 0x34, 0x64, 0x59, 0x48, 0x6d, 0x4c, 0x57, 0x53, 0x78, 0x74, 0x77, 0x72, 0x7a, 0x45, 0x6e, 0x51, 0x4a, 0x5f, 0x66, 0x5a, 0x49, 0x4a, 0x34, 0x30, 0x4e, 0x67, 0x37, 0x35, 0x43, 0x79, 0x53, 0x48, 0x50, 0x63, 0x34, 0x76, 0x58, 0x62, 0x6d, 0x71, 0x78, 0x26, 0x55, 0x45, 0x35, 0x33, 0x30, 0x52, 0x68, 0x30, 0x69, 0x43, 0x44, 0x35, 0x4b, 0x5a, 0x66, 0x42, 0x31, 0x44, 0x61, 0x33, 0x52, 0x36, 0x6f, 0x35, 0x64, 0x5f, 0x6b, 0x4a, 0x5f, 0x6a, 0x7a, 0x5a, 0x59, 0x4d, 0x68, 0x4b, 0x69, 0x78, 0x78, 0x32, 0x65, 0x75, 0x42, 0x6e, 0x6f, 0x67, 0x58, 0x5a, 0x69, 0x48, 0x32, 0x43, 0x56, 0x49, 0x39, 0x79, 0x36, 0x4a, 0x31, 0x61, 0x4a, 0x68, 0x59, 0x48, 0x65, 0x5a, 0x46, 0x36, 0x35, 0x39, 0x36, 0x30, 0x71, 0x67, 0x4c, 0x69, 0x55, 0x42, 0x73, 0x46, 0x79, 0x39, 0x6b, 0x35, 0x4a, 0x63, 0x44, 0x4f, 0x67, 0x69, 0x65, 0x53, 0x38, 0x33, 0x76, 0x69, 0xa5, 0x61, 0x71, 0x67, 0x4b, 0x36, 0x6e, 0x79, 0x62, 0x56, 0x32, 0x62, 0x62, 0x54, 0x66, 0x71, 0x6e, 0x32, 0x77, 0x71, 0x61, 0x62, 0x50, 0x52, 0x64, 0x57, 0x4a, 0x6d, 0x43, 0x67, 0x65, 0x42, 0x73, 0x6a, 0x6c, 0x31, 0x30, 0x66, 0x47, 0x73, 0x4e, 0x6f, 0x67, 0x64, 0x67, 0x4e, 0x32, 0x54, 0x37, 0x66, 0x76, 0x74, 0xa1, 0x61, 0x7a, 0x66, 0x6b, 0x49, 0x58, 0x38, 0x4d, 0x74, 0x68, 0x69, 0x42, 0x6a, 0x73, 0x30, 0x50, 0x61, 0x59, 0x78, 0x37, 0x66, 0x6d, 0x66, 0x52, 0x47, 0x32, 0x75, 0x45, 0x32, 0x78, 0x75, 0x6a, 0x58, 0x6b, 0x56, 0x4c, 0x52, 0x79, 0x33, 0x73, 0x6a, 0x5a, 0x7a, 0x6a, 0x52, 0x47, 0x50, 0x75, 0x43, 0x74, 0x63, 0x49, 0x6b, 0x55, 0x73, 0x31, 0x58, 0x45, 0x5a, 0x49, 0x64, 0x47, 0x31, 0x43, 0x61, 0x5a, 0x59, 0x62, 0x69, 0x47, 0x4e, 0x58, 0x67, 0x65, 0x68, 0x6e, 0x44, 0x48, 0x48, 0x4f, 0x5a, 0x78, 0x53, 0x31, 0x59, 0x70, 0x70, 0x5f, 0x34, 0x69, 0x78, 0x27, 0x42, 0x4d, 0x45, 0x66, 0x38, 0x72, 0x6d, 0x69, 0x53, 0x74, 0x6a, 0x49, 0x30, 0x6f, 0x6e, 0x73, 0x6e, 0x58, 0x78, 0x54, 0x79, 0x76, 0x31, 0x44, 0x4e, 0x33, 0x50, 0x79, 0x51, 0x6b, 0x72, 0x52, 0x74, 0x71, 0x66, 0x63, 0x6d, 0x57, 0x4a, 0x61, 0x4b, 0x78, 0x25, 0x35, 0x51, 0x6b, 0x4a, 0x34, 0x32, 0x78, 0x4f, 0x50, 0x6d, 0x54, 0x34, 0x66, 0x79, 0x43, 0x30, 0x6d, 0x4d, 0x61, 0x75, 0x64, 0x41, 0x47, 0x58, 0x4e, 0x72, 0x53, 0x6c, 0x4c, 0x78, 0x31, 0x76, 0x69, 0x74, 0x39, 0x4c, 0x6f, 0x72, 0x45, 0x55, 0x4d, 0x76, 0x71, 0x31, 0x44, 0x41, 0x38, 0x54, 0x69, 0x59, 0x49, 0x45, 0x4a, 0x67, 0x6f, 0x67, 0x78, 0x1b, 0x35, 0x68, 0x75, 0x62, 0x4d, 0x6b, 0x34, 0x41, 0x54, 0x63, 0x74, 0x76, 0x54, 0x37, 0x51, 0x41, 0x73, 0x4c, 0x41, 0x5f, 0x4a, 0x36, 0x38, 0x57, 0x70, 0x5f, 0x4b, 0x78, 0x1a, 0x58, 0x4c, 0x6a, 0x44, 0x54, 0x59, 0x67, 0x72, 0x4e, 0x6e, 0x43, 0x46, 0x41, 0x33, 0x70, 0x78, 0x73, 0x6a, 0x35, 0x69, 0x4f, 0x36, 0x39, 0x49, 0x4c, 0x31, 0x78, 0x22, 0x4d, 0x4f, 0x4a, 0x65, 0x6a, 0x53, 0x6e, 0x6a, 0x54, 0x50, 0x65, 0x58, 0x61, 0x56, 0x63, 0x31, 0x51, 0x44, 0x4e, 0x71, 0x30, 0x5a, 0x31, 0x79, 0x33, 0x6d, 0x4e, 0x55, 0x4e, 0x71, 0x51, 0x46, 0x62, 0x73, 0x73, 0x37, 0x31, 0x49, 0x52, 0x70, 0x68, 0x46, 0x32, 0x5a, 0x59, 0x68, 0x53, 0x67, 0x56, 0x69, 0x53, 0x73, 0x6d, 0x4d, 0x78, 0x4a, 0x77, 0x4b, 0x66, 0x55, 0x65, 0x66, 0x4c, 0x34, 0x74, 0x59, 0x4a, 0x76, 0x4d, 0x33, 0x4d, 0x51, 0x36, 0x69, 0x63, 0x6c, 0x73, 0x44, 0x50, 0x56, 0x68, 0x78, 0x4a, 0x39, 0x4e, 0x73, 0x6c, 0x73, 0x4d, 0x71, 0x38, 0x6d, 0x4c, 0x4a, 0x48, 0x43, 0x69, 0x34, 0x62, 0x65, 0x54, 0x78, 0x69, 0x6b, 0x50, 0x6e, 0x4f, 0x4b, 0x4b, 0x5a, 0x70, 0x65, 0x38, 0x55, 0x59, 0x73, 0x54, 0x43, 0x48, 0x42, 0x51, 0x67, 0x78, 0x64, 0x4e, 0x56, 0x6f, 0x55, 0x71, 0x61, 0x78, 0x1d, 0x41, 0x6a, 0x68, 0x66, 0x79, 0x36, 0x64, 0x36, 0x36, 0x72, 0x77, 0x68, 0x59, 0x54, 0x48, 0x4b, 0x45, 0x66, 0x57, 0x4c, 0x6a, 0x65, 0x37, 0x63, 0x39, 0x6e, 0x64, 0x6e, 0x45, 0x78, 0x45, 0x64, 0x76, 0x54, 0x7a, 0x34, 0x76, 0x61, 0x61, 0x77, 0x64, 0x79, 0x79, 0x75, 0x45, 0x35, 0x77, 0x39, 0x53, 0x62, 0x73, 0x44, 0x6c, 0x6f, 0x76, 0x71, 0x39, 0x52, 0x71, 0x6d, 0x68, 0x4f, 0x59, 0x30, 0x32, 0x43, 0x45, 0x51, 0x69, 0x44, 0x75, 0x52, 0x73, 0x41, 0x61, 0x62, 0x61, 0x47, 0x70, 0x32, 0x56, 0x68, 0x77, 0x4d, 0x69, 0x44, 0x61, 0x66, 0x4d, 0x74, 0x77, 0x67, 0x62, 0x7a, 0x53, 0x7a, 0x67, 0x33, 0x36, 0x36, 0x78, 0x2c, 0x4a, 0x31, 0x6b, 0x4f, 0x41, 0x35, 0x4c, 0x6b, 0x50, 0x68, 0x33, 0x73, 0x72, 0x50, 0x61, 0x4d, 0x78, 0x70, 0x58, 0x72, 0x7a, 0x73, 0x6b, 0x5f, 0x34, 0x5f, 0x37, 0x44, 0x50, 0x37, 0x4d, 0x39, 0x7a, 0x67, 0x65, 0x57, 0x4a, 0x4c, 0x6f, 0x67, 0x32, 0x37, 0x34, 0x61, 0x78, 0x42, 0x49, 0x79, 0x74, 0x36, 0x6b, 0x54, 0x78, 0x50, 0x63, 0x73, 0x34, 0x4c, 0x49, 0x56, 0x73, 0x78, 0x52, 0x47, 0x38, 0x65, 0x45, 0x34, 0x44, 0x63, 0x70, 0x73, 0x57, 0x55, 0x31, 0x57, 0x37, 0x59, 0x51, 0x36, 0x74, 0x59, 0x70, 0x69, 0x4d, 0x41, 0x5a, 0x58, 0x47, 0x56, 0x56, 0x49, 0x53, 0x4e, 0x53, 0x46, 0x63, 0x6c, 0x4f, 0x31, 0x68, 0x67, 0x6e, 0x78, 0x6d, 0x38, 0x4d, 0x49, 0x61, 0x6e, 0x54, 0x69, 0x71, 0x4d, 0x67, 0x79, 0x4a, 0x41, 0x71, 0x63, 0x30, 0x7a, 0x4a, 0x4e, 0x4e, 0x39, 0x39, 0x46, 0x45, 0x4b, 0xa2, 0x61, 0x32, 0x65, 0x58, 0x5f, 0x4d, 0x4e, 0x57, 0x61, 0x79, 0x6e, 0x5a, 0x68, 0x4b, 0x6d, 0x47, 0x65, 0x55, 0x49, 0x6d, 0x79, 0x72, 0x4a, 0x42, 0x4e, 0x6f, 0x49, 0x43, 0x68, 0x4b, 0x66, 0x61, 0x7a, 0x35, 0x79, 0x33, 0x67, 0x43, 0x70, 0x5a, 0x66, 0xa3, 0x61, 0x6f, 0x60, 0x63, 0x41, 0x4a, 0x55, 0x6f, 0x61, 0x75, 0x71, 0x78, 0x64, 0x58, 0x6f, 0x48, 0x39, 0x54, 0x72, 0x5a, 0x70, 0x30, 0x34, 0x62, 0x79, 0x35, 0xa1, 0x61, 0x4b, 0x6b, 0x49, 0x4a, 0x4f, 0x49, 0x70, 0x65, 0x58, 0x54, 0x75, 0x4e, 0x4d, 0x78, 0x2b, 0x71, 0x67, 0x67, 0x74, 0x4a, 0x77, 0x49, 0x76, 0x55, 0x67, 0x77, 0x69, 0x32, 0x38, 0x31, 0x4d, 0x51, 0x4f, 0x6c, 0x4c, 0x6e, 0x67, 0x4a, 0x46, 0x51, 0x77, 0x54, 0x66, 0x67, 0x78, 0x58, 0x38, 0x6a, 0x7a, 0x61, 0x56, 0x77, 0x31, 0x39, 0x78, 0x35, 0x46, 0x48, 0x78, 0x48, 0x68, 0x30, 0x74, 0x37, 0x68, 0x77, 0x5a, 0x42, 0x7a, 0x4e, 0x49, 0x41, 0x58, 0x7a, 0x73, 0x7a, 0x58, 0x51, 0x35, 0x6f, 0x63, 0x47, 0x57, 0x37, 0x79, 0x64, 0x4e, 0x49, 0x41, 0x69, 0x70, 0x46, 0x52, 0x46, 0x4e, 0x5f, 0x44, 0x77, 0x48, 0x4f, 0x34, 0x72, 0x77, 0x33, 0x58, 0x62, 0x58, 0x79, 0x53, 0x75, 0x4b, 0x47, 0x74, 0x6e, 0x4d, 0x64, 0x78, 0x30, 0x68, 0x64, 0x64, 0x54, 0x35, 0x66, 0x4f, 0x34, 0x68, 0x33, 0x4d, 0x71, 0x69, 0x33, 0x78, 0x20, 0x6f, 0x63, 0x36, 0x4f, 0x53, 0x57, 0x6c, 0x43, 0x68, 0x31, 0x44, 0x68, 0x54, 0x34, 0x77, 0x33, 0x4b, 0x58, 0x71, 0x39, 0x58, 0x42, 0x62, 0x55, 0x59, 0x74, 0x56, 0x47, 0x33, 0x6a, 0x59, 0x49, 0x78, 0x2e, 0x76, 0x67, 0x4e, 0x38, 0x79, 0x39, 0x63, 0x67, 0x33, 0x31, 0x59, 0x5f, 0x58, 0x79, 0x4d, 0x71, 0x57, 0x4a, 0x77, 0x45, 0x4a, 0x6e, 0x63, 0x4d, 0x36, 0x47, 0x43, 0x78, 0x36, 0x59, 0x32, 0x6c, 0x33, 0x63, 0x32, 0x55, 0x46, 0x59, 0x54, 0x6e, 0x4d, 0x4d, 0x62, 0x6e, 0x6c, 0x4b, 0x65, 0x71, 0x68, 0x39, 0x31, 0x74, 0xa3, 0x61, 0x6f, 0x66, 0x55, 0x61, 0x68, 0x73, 0x53, 0x6a, 0x61, 0x57, 0x67, 0x45, 0x75, 0x61, 0x4e, 0x66, 0x50, 0x6c, 0x62, 0x4d, 0x63, 0x6d, 0x73, 0x30, 0x77, 0x31, 0x49, 0x52, 0x6d, 0x48, 0x49, 0x4f, 0x4c, 0x77, 0x58, 0x6f, 0x66, 0x58, 0x70, 0x43, 0x4a, 0x30, 0x4f, 0x53, 0x78, 0x74, 0x5f, 0x69, 0x73, 0x77, 0x71, 0x78, 0x40, 0x65, 0x7a, 0x50, 0x46, 0x59, 0x5a, 0x65, 0x62, 0x39, 0x74, 0x6a, 0x35, 0x67, 0x54, 0x49, 0x4f, 0x7a, 0x41, 0x67, 0x63, 0x47, 0x57, 0x69, 0x4b, 0x6d, 0x75, 0x65, 0x39, 0x6b, 0x51, 0x4c, 0x70, 0x73, 0x69, 0x74, 0x70, 0x4e, 0x43, 0x65, 0x4a, 0x6f, 0x61, 0x69, 0x64, 0x48, 0x36, 0x4a, 0x5f, 0x32, 0x6b, 0x57, 0x50, 0x63, 0x34, 0x76, 0x7a, 0x53, 0x67, 0x4c, 0x37, 0x38, 0x71, 0x6d, 0x56, 0x78, 0x2f, 0x56, 0x70, 0x42, 0x46, 0x64, 0x72, 0x64, 0x39, 0x46, 0x61, 0x5f, 0x73, 0x76, 0x49, 0x31, 0x5a, 0x77, 0x5f, 0x75, 0x67, 0x4d, 0x7a, 0x38, 0x75, 0x59, 0x5f, 0x6a, 0x57, 0x52, 0x41, 0x77, 0x46, 0x34, 0x38, 0x65, 0x62, 0x61, 0x70, 0x5f, 0x65, 0x42, 0x62, 0x5a, 0x30, 0x4b, 0x72, 0x35, 0x78, 0x30, 0x6a, 0x61, 0x67, 0x45, 0x57, 0x4c, 0x49, 0x4f, 0x62, 0x5f, 0x42, 0x4f, 0x6f, 0x6f, 0x58, 0x61, 0x45, 0x78, 0x64, 0x4c, 0x75, 0x59, 0x52, 0x58, 0x79, 0x74, 0x73, 0x4f, 0x6f, 0x6c, 0x77, 0x4e, 0x6c, 0x75, 0x6e, 0x5f, 0x77, 0x61, 0x6b, 0x6b, 0x36, 0x41, 0x56, 0x74, 0x6a, 0x33, 0x4d, 0x49, 0x78, 0x2b, 0x4e, 0x4a, 0x4d, 0x46, 0x52, 0x58, 0x7a, 0x4a, 0x6e, 0x52, 0x65, 0x72, 0x4a, 0x32, 0x5f, 0x6f, 0x4f, 0x54, 0x6c, 0x59, 0x58, 0x42, 0x74, 0x6b, 0x42, 0x4c, 0x49, 0x48, 0x33, 0x73, 0x31, 0x4b, 0x69, 0x65, 0x78, 0x65, 0x46, 0x32, 0x63, 0x74, 0x51, 0x5a, 0x45, 0x78, 0x39, 0x61, 0x46, 0x69, 0x59, 0x39, 0x64, 0x34, 0x46, 0x39, 0x48, 0x6b, 0x63, 0x36, 0x75, 0x31, 0x7a, 0x79, 0x58, 0x65, 0x4e, 0x4b, 0x31, 0x6d, 0x48, 0x48, 0x61, 0x6f, 0x44, 0x74, 0x39, 0x78, 0x6b, 0x55, 0x4b, 0x69, 0x4d, 0x61, 0x73, 0x65, 0x4f, 0x47, 0x37, 0x63, 0x62, 0x44, 0x58, 0x43, 0x76, 0x61, 0x4a, 0x4e, 0x73, 0x4e, 0x78, 0x42, 0x4d, 0x67, 0x75, 0x78, 0x6c, 0x43, 0x34, 0x41, 0x33, 0x49, 0x6e, 0x69, 0x61, 0x48, 0x54, 0x78, 0x63, 0x6b, 0x57, 0x63, 0x6b, 0x71, 0x59, 0x75, 0x78, 0x35, 0x63, 0x75, 0x38, 0x6e, 0x32, 0x62, 0x6f, 0x61, 0x45, 0x5a, 0x53, 0x50, 0x48, 0x4e, 0x73, 0x43, 0x62, 0x44, 0x33, 0x70, 0x74, 0x7a, 0x43, 0x53, 0x45, 0x39, 0x7a, 0x35, 0x56, 0x6d, 0x65, 0x43, 0x30, 0x66, 0x61, 0x39, 0x4d, 0x63, 0x31, 0x70, 0x73, 0x62, 0x39, 0x4f, 0x32, 0x4f, 0x4e, 0x48, 0x51, 0x49, 0x56, 0x4d, 0x59, 0x62, 0x71, 0x38, 0xa3, 0x61, 0x78, 0x67, 0x6e, 0x70, 0x52, 0x74, 0x45, 0x4c, 0x66, 0x68, 0x70, 0x6e, 0x44, 0x69, 0x77, 0x54, 0x7a, 0x49, 0x68, 0x5f, 0x55, 0x67, 0x52, 0x61, 0x4c, 0x59, 0x37, 0x62, 0x6d, 0x55, 0x64, 0x64, 0x5f, 0x6c, 0x56, 0x65, 0x47, 0x4e, 0x4a, 0x57, 0x53, 0x78, 0x47, 0x56, 0x45, 0x34, 0x78, 0x70, 0x65, 0x55, 0x42, 0x37, 0x6c, 0x59, 0x4b, 0x73, 0x76, 0x5a, 0x67, 0x77, 0x31, 0x71, 0x4c, 0x39, 0x57, 0x75, 0x59, 0x36, 0x52, 0x6d, 0x6b, 0x5a, 0x70, 0x73, 0x51, 0x37, 0x30, 0x45, 0x56, 0x70, 0x4d, 0x32, 0x4f, 0x48, 0x67, 0x4b, 0x7a, 0x33, 0x45, 0x65, 0x76, 0x58, 0x30, 0x67, 0x33, 0x46, 0x54, 0x30, 0x35, 0x37, 0x53, 0x4b, 0x66, 0x58, 0x47, 0x6d, 0x69, 0x69, 0x5f, 0x64, 0x41, 0x50, 0x62, 0x6f, 0x73, 0x71, 0x6c, 0x4a, 0x56, 0x54, 0x77, 0x4a, 0x53, 0x63, 0x55, 0x42, 0x65, 0x4f, 0x52, 0x6b, 0x76, 0x78, 0x71, 0x6a, 0xa2, 0x61, 0x6c, 0x61, 0x57, 0x65, 0x4e, 0x66, 0x6a, 0x5f, 0x64, 0xa1, 0x61, 0x78, 0x63, 0x41, 0x42, 0x59, 0x65, 0x4a, 0x77, 0x58, 0x4a, 0x75, 0xa7, 0x61, 0x32, 0x60, 0x62, 0x5f, 0x6e, 0x64, 0x61, 0x54, 0x52, 0x41, 0x64, 0x34, 0x32, 0x5f, 0x53, 0x64, 0x4f, 0x6a, 0x42, 0x62, 0x64, 0x56, 0x63, 0x55, 0x64, 0xa1, 0x61, 0x6a, 0x63, 0x59, 0x70, 0x51, 0x66, 0x77, 0x6f, 0x53, 0x74, 0x76, 0x72, 0xa1, 0x61, 0x59, 0x61, 0x59, 0x65, 0x4a, 0x74, 0x6c, 0x57, 0x4a, 0x6e, 0x77, 0x43, 0x63, 0x39, 0x79, 0x78, 0x34, 0x47, 0x47, 0x72, 0x35, 0x48, 0x68, 0x73, 0x66, 0x4c, 0x72, 0x71, 0x6e, 0x67, 0x6a, 0x67, 0x31, 0x41, 0x36, 0x30, 0x4e, 0x34, 0x35, 0x78, 0x2f, 0x51, 0x42, 0x5a, 0x4c, 0x67, 0x35, 0x37, 0x74, 0x39, 0x42, 0x35, 0x55, 0x59, 0x58, 0x34, 0x5f, 0x72, 0x4d, 0x37, 0x33, 0x78, 0x51, 0x61, 0x6d, 0x68, 0x7a, 0x41, 0x30, 0x30, 0x66, 0x6b, 0x5f, 0x39, 0x57, 0x30, 0x42, 0x68, 0x38, 0x4d, 0x54, 0x72, 0x6f, 0x61, 0x64, 0x73, 0x4e, 0x4a, 0xa4, 0x61, 0x31, 0x69, 0x46, 0x72, 0x43, 0x64, 0x73, 0x69, 0x56, 0x34, 0x53, 0x61, 0x51, 0x65, 0x57, 0x36, 0x53, 0x42, 0x65, 0x62, 0x67, 0x46, 0x70, 0x43, 0x55, 0x59, 0x4e, 0x6a, 0x58, 0x63, 0x38, 0x4c, 0x47, 0x36, 0x37, 0x58, 0x41, 0x30, 0x5a, 0x66, 0x58, 0x37, 0x38, 0x59, 0x7a, 0x59, 0x69, 0x75, 0x38, 0x55, 0x49, 0x56, 0x6b, 0x71, 0x64, 0x47, 0x69, 0x6f, 0x59, 0x59, 0x53, 0x54, 0x76, 0x77, 0x76, 0x34, 0x78, 0x33, 0x32, 0x58, 0x70, 0x31, 0x43, 0x6f, 0x78, 0x46, 0x50, 0x71, 0x62, 0x50, 0x39, 0x5a, 0x31, 0x36, 0x43, 0x5f, 0x45, 0x47, 0x42, 0x65, 0x71, 0x47, 0x6e, 0x7a, 0x63, 0x62, 0x31, 0x78, 0x56, 0x32, 0x4c, 0x47, 0x79, 0x51, 0x48, 0x72, 0x30, 0x57, 0x71, 0x67, 0x71, 0x76, 0x70, 0x65, 0x48, 0x4e, 0x4b, 0x42, 0x39, 0x74, 0x67, 0x5a, 0x67, 0x75, 0x63, 0x4d, 0x62, 0x59, 0x4a, 0x7a, 0x64, 0x57, 0x79, 0x53, 0x6b, 0x64, 0x6e, 0x6d, 0x68, 0x5a, 0x78, 0x18, 0x45, 0x4c, 0x52, 0x4c, 0x50, 0x79, 0x43, 0x6f, 0x33, 0x51, 0x63, 0x75, 0x63, 0x39, 0x56, 0x49, 0x62, 0x45, 0x7a, 0x42, 0x6b, 0x5a, 0x4a, 0x35, 0x78, 0x24, 0x6f, 0x77, 0x6c, 0x51, 0x79, 0x4e, 0x56, 0x4f, 0x74, 0x44, 0x64, 0x6d, 0x32, 0x67, 0x42, 0x4a, 0x52, 0x45, 0x53, 0x4f, 0x6b, 0x63, 0x31, 0x73, 0x4a, 0x33, 0x37, 0x57, 0x35, 0x70, 0x30, 0x55, 0x6c, 0x35, 0x77, 0x41, 0x78, 0x4b, 0x71, 0x54, 0x69, 0x52, 0x30, 0x61, 0x72, 0x41, 0x4c, 0x46, 0x33, 0x45, 0x67, 0x73, 0x49, 0x4e, 0x45, 0x5f, 0x6a, 0x54, 0x48, 0x4b, 0x76, 0x5f, 0x53, 0x39, 0x55, 0x53, 0x55, 0x30, 0x57, 0x4d, 0x67, 0x48, 0x57, 0x48, 0x6c, 0x30, 0x31, 0x37, 0x35, 0x42, 0x30, 0x38, 0x39, 0x42, 0x52, 0x35, 0x41, 0x44, 0x74, 0x7a, 0x47, 0x78, 0x79, 0x46, 0x43, 0x72, 0x46, 0x4e, 0x41, 0x4d, 0x48, 0x79, 0x62, 0x52, 0x4d, 0x49, 0x6f, 0x73, 0x46, 0x4f, 0x33, 0x6f, 0x73, 0x78, 0x1c, 0x6e, 0x48, 0x49, 0x73, 0x6a, 0x58, 0x54, 0x55, 0x32, 0x39, 0x67, 0x68, 0x54, 0x6d, 0x72, 0x34, 0x59, 0x59, 0x5f, 0x32, 0x33, 0x51, 0x76, 0x69, 0x37, 0x5f, 0x6f, 0x5a, 0x78, 0x22, 0x72, 0x54, 0x73, 0x52, 0x30, 0x33, 0x43, 0x6f, 0x54, 0x57, 0x6c, 0x41, 0x5f, 0x6e, 0x55, 0x31, 0x6e, 0x67, 0x67, 0x4b, 0x41, 0x37, 0x64, 0x70, 0x69, 0x4a, 0x59, 0x48, 0x59, 0x45, 0x48, 0x6a, 0x43, 0x56, 0x66, 0x51, 0x36, 0x6a, 0x57, 0x64, 0x64, 0x78, 0x29, 0x61, 0x79, 0x73, 0x71, 0x4b, 0x64, 0x6a, 0x79, 0x6a, 0x6a, 0x54, 0x76, 0x43, 0x45, 0x4d, 0x72, 0x55, 0x78, 0x37, 0x43, 0x45, 0x6e, 0x76, 0x74, 0x32, 0x37, 0x66, 0x57, 0x51, 0x61, 0x57, 0x68, 0x37, 0x73, 0x35, 0x34, 0x69, 0x36, 0x70, 0x41, 0x41, 0x78, 0x26, 0x36, 0x70, 0x4a, 0x79, 0x4d, 0x72, 0x63, 0x45, 0x78, 0x36, 0x5f, 0x7a, 0x62, 0x61, 0x32, 0x4b, 0x37, 0x57, 0x35, 0x72, 0x34, 0x55, 0x77, 0x34, 0x56, 0x67, 0x69, 0x4a, 0x35, 0x47, 0x6d, 0x55, 0x30, 0x4d, 0x6e, 0x7a, 0x51, 0x6e, 0xa5, 0x61, 0x41, 0x6a, 0x61, 0x53, 0x35, 0x39, 0x43, 0x44, 0x76, 0x47, 0x56, 0x79, 0x62, 0x49, 0x61, 0xa1, 0x61, 0x76, 0x61, 0x47, 0x64, 0x7a, 0x46, 0x43, 0x4d, 0x6f, 0x67, 0x5f, 0x32, 0x6f, 0x63, 0x53, 0x79, 0x79, 0x79, 0x4b, 0x68, 0x43, 0x54, 0x31, 0x31, 0x64, 0x4b, 0x74, 0x77, 0x59, 0x68, 0x75, 0x36, 0x6c, 0x65, 0x64, 0x4c, 0x62, 0x65, 0x61, 0x4c, 0x6a, 0x6e, 0x32, 0x6d, 0x69, 0x57, 0x65, 0x43, 0x48, 0x36, 0x78, 0x78, 0x33, 0x39, 0x34, 0x6d, 0x58, 0x74, 0x6c, 0x34, 0x49, 0x42, 0x52, 0x44, 0x61, 0x37, 0x62, 0x6a, 0x6b, 0x5f, 0x49, 0x72, 0x47, 0x62, 0x78, 0x6c, 0x6b, 0x4e, 0x57, 0x72, 0x64, 0x6b, 0x48, 0x49, 0x58, 0x55, 0x4c, 0x6c, 0x30, 0x56, 0x61, 0x73, 0x35, 0x61, 0x32, 0x71, 0x69, 0x4d, 0x79, 0x78, 0x71, 0x57, 0x77, 0x44, 0x78, 0x37, 0x74, 0x41, 0x72, 0x70, 0x4c, 0x37, 0x4f, 0x45, 0x54, 0x46, 0x6d, 0x48, 0x5f, 0x78, 0x56, 0x57, 0x55, 0x64, 0x32, 0x4b, 0x68, 0x46, 0x68, 0x73, 0x6a, 0x6a, 0x43, 0x6c, 0x5f, 0x43, 0x62, 0x54, 0x75, 0x47, 0x48, 0x4c, 0x41, 0x6e, 0x34, 0x75, 0x4e, 0x58, 0x4c, 0x61, 0x73, 0x42, 0x31, 0x45, 0x54, 0x6d, 0x7a, 0x4a, 0x6e, 0x57, 0x4f, 0x78, 0x22, 0x6e, 0x4e, 0x4b, 0x63, 0x41, 0x65, 0x7a, 0x69, 0x6d, 0x78, 0x72, 0x71, 0x63, 0x6e, 0x53, 0x35, 0x59, 0x6c, 0x5f, 0x45, 0x6d, 0x48, 0x32, 0x4a, 0x56, 0x63, 0x4e, 0x53, 0x67, 0x56, 0x50, 0x53, 0x44, 0x75, 0x78, 0x2a, 0x37, 0x52, 0x71, 0x42, 0x61, 0x35, 0x4e, 0x76, 0x6e, 0x6a, 0x69, 0x62, 0x51, 0x67, 0x31, 0x44, 0x64, 0x4e, 0x4c, 0x31, 0x62, 0x43, 0x66, 0x4c, 0x49, 0x62, 0x67, 0x68, 0x58, 0x68, 0x36, 0x34, 0x70, 0x68, 0x52, 0x61, 0x61, 0x50, 0x68, 0x49, 0x5f, 0x63, 0x78, 0x1f, 0x31, 0x31, 0x38, 0x73, 0x45, 0x77, 0x35, 0x34, 0x41, 0x6b, 0x72, 0x72, 0x78, 0x69, 0x36, 0x68, 0x71, 0x51, 0x31, 0x72, 0x56, 0x34, 0x69, 0x42, 0x73, 0x71, 0x76, 0x79, 0x6a, 0x57, 0x4a, 0xa4, 0x61, 0x41, 0x63, 0x44, 0x63, 0x69, 0x65, 0x31, 0x61, 0x51, 0x34, 0x4d, 0xa2, 0x61, 0x34, 0x64, 0x75, 0x33, 0x74, 0x4a, 0x61, 0x59, 0x62, 0x33, 0x78, 0x61, 0x4a, 0x70, 0x6f, 0x58, 0x73, 0x74, 0x45, 0x30, 0x5a, 0x44, 0x6d, 0x39, 0x6d, 0x44, 0x55, 0x6c, 0x63, 0x52, 0x63, 0x4c, 0x67, 0x4d, 0x70, 0x7a, 0x77, 0x37, 0x77, 0x43, 0x41, 0x30, 0x4b, 0x33, 0x74, 0x32, 0x57, 0x4a, 0x4d, 0x30, 0x31, 0x78, 0x20, 0x6b, 0x70, 0x57, 0x6d, 0x71, 0x74, 0x6c, 0x69, 0x7a, 0x45, 0x75, 0x53, 0x65, 0x4f, 0x39, 0x50, 0x79, 0x4d, 0x38, 0x72, 0x74, 0x39, 0x44, 0x67, 0x65, 0x72, 0x77, 0x30, 0x33, 0x39, 0x4e, 0x52, 0x78, 0x50, 0x31, 0x52, 0x51, 0x35, 0x5f, 0x42, 0x50, 0x57, 0x39, 0x79, 0x6b, 0x77, 0x4d, 0x75, 0x47, 0x55, 0x45, 0x69, 0x4e, 0x69, 0x53, 0x41, 0x4d, 0x48, 0x30, 0x42, 0x6b, 0x47, 0x48, 0x73, 0x30, 0x4d, 0x6d, 0x70, 0x67, 0x42, 0x46, 0x39, 0x4a, 0x31, 0x43, 0x47, 0x49, 0x72, 0x69, 0x72, 0x42, 0x75, 0x42, 0x78, 0x45, 0x33, 0x37, 0x64, 0x42, 0x46, 0x6d, 0x4d, 0x6b, 0x4e, 0x6a, 0x78, 0x43, 0x49, 0x30, 0x4f, 0x36, 0x6b, 0x6d, 0x4c, 0x74, 0x58, 0x68, 0x53, 0x76, 0x39, 0x75, 0x4a, 0x62, 0x36, 0x6c, 0x79, 0x46, 0x48, 0x5f, 0x49, 0x42, 0x65, 0x56, 0x54, 0x42, 0x78, 0x50, 0x78, 0x5a, 0x73, 0x58, 0x78, 0x4f, 0x34, 0x44, 0x68, 0x46, 0x55, 0x55, 0x55, 0x35, 0x72, 0x6a, 0x47, 0x69, 0x50, 0x4d, 0x36, 0x30, 0x36, 0x67, 0x6a, 0x6c, 0x66, 0x42, 0x4f, 0x64, 0x4f, 0x53, 0x57, 0x56, 0x38, 0x54, 0x64, 0x53, 0x73, 0x70, 0x36, 0x31, 0x36, 0x48, 0x4d, 0x6b, 0x50, 0x68, 0x39, 0x6e, 0x5a, 0x56, 0x4f, 0x62, 0x36, 0x74, 0x41, 0x73, 0x37, 0x4c, 0x67, 0x4b, 0x42, 0x6f, 0x4e, 0x7a, 0x74, 0x50, 0x55, 0x58, 0x7a, 0x54, 0x43, 0x54, 0x64, 0x49, 0x33, 0x65, 0x70, 0x78, 0x6b, 0x7a, 0x35, 0x53, 0x70, 0x62, 0x5a, 0x69, 0x4a, 0x68, 0x52, 0x78, 0x63, 0x39, 0x53, 0x42, 0x78, 0x64, 0x69, 0x34, 0x58, 0x30, 0x6f, 0x32, 0x70, 0x4f, 0x78, 0x63, 0x52, 0x36, 0x30, 0x62, 0x51, 0x67, 0x76, 0x46, 0x77, 0x50, 0x33, 0x58, 0x41, 0x64, 0x54, 0x36, 0x41, 0x62, 0x37, 0x51, 0x6b, 0x34, 0x77, 0x63, 0x59, 0x43, 0x4e, 0x50, 0x49, 0x65, 0x73, 0x72, 0x34, 0x42, 0x35, 0x65, 0x47, 0x6e, 0x63, 0x4a, 0x32, 0x6f, 0x32, 0x4d, 0x34, 0x36, 0x66, 0x51, 0x5a, 0x73, 0x6f, 0x4f, 0x76, 0x6a, 0x6a, 0x41, 0x4c, 0x39, 0x6d, 0x4c, 0x4d, 0x30, 0x50, 0x38, 0x6b, 0x74, 0x4f, 0x33, 0x6d, 0x31, 0x62, 0x47, 0x32, 0x57, 0x78, 0x59, 0x56, 0x42, 0x30, 0x68, 0x48, 0x38, 0x58, 0x35, 0x6c, 0x4e, 0x6f, 0x35, 0x6f, 0x64, 0x78, 0x1c, 0x4d, 0x58, 0x51, 0x4d, 0x4d, 0x4b, 0x5a, 0x48, 0x75, 0x73, 0x55, 0x7a, 0x6a, 0x70, 0x50, 0x78, 0x41, 0x32, 0x6f, 0x55, 0x43, 0x4e, 0x61, 0x6d, 0x73, 0x77, 0x4c, 0x76, 0x78, 0x2b, 0x68, 0x48, 0x74, 0x6f, 0x49, 0x52, 0x30, 0x44, 0x6a, 0x69, 0x64, 0x43, 0x56, 0x63, 0x30, 0x37, 0x46, 0x78, 0x49, 0x65, 0x6f, 0x48, 0x6a, 0x6d, 0x71, 0x59, 0x47, 0x6d, 0x54, 0x33, 0x5f, 0x36, 0x56, 0x4a, 0x31, 0x72, 0x37, 0x76, 0x4f, 0x79, 0x5a, 0x5a, 0x76, 0x78, 0x2d, 0x44, 0x49, 0x33, 0x57, 0x66, 0x6d, 0x53, 0x4e, 0x36, 0x4a, 0x67, 0x6a, 0x54, 0x6b, 0x32, 0x59, 0x41, 0x55, 0x4b, 0x46, 0x33, 0x68, 0x78, 0x32, 0x5f, 0x4c, 0x46, 0x72, 0x7a, 0x31, 0x78, 0x4b, 0x73, 0x37, 0x31, 0x67, 0x52, 0x4e, 0x61, 0x4d, 0x79, 0x77, 0x6a, 0x6d, 0x31, 0x78, 0x4b, 0x49, 0x53, 0x57, 0x55, 0x6b, 0x70, 0x43, 0x45, 0x4d, 0x4b, 0x59, 0x32, 0x5f, 0x49, 0x59, 0x78, 0x39, 0x4a, 0x4a, 0x68, 0x44, 0x51, 0x32, 0x43, 0x4f, 0x58, 0x35, 0x73, 0x4f, 0x45, 0x35, 0x44, 0x58, 0x31, 0x34, 0x7a, 0x54, 0x4a, 0x75, 0x41, 0x34, 0x74, 0x72, 0x4c, 0x34, 0x31, 0x47, 0x55, 0x51, 0x50, 0x37, 0x55, 0x79, 0x79, 0x5f, 0x46, 0x30, 0x5f, 0x36, 0x33, 0x63, 0x4f, 0x36, 0x6c, 0x6c, 0x4c, 0x56, 0x64, 0x41, 0x39, 0x73, 0x5a, 0x45, 0x49, 0x31, 0x63, 0x53, 0x56, 0x73, 0x78, 0x22, 0x30, 0x48, 0x46, 0x47, 0x78, 0x31, 0x56, 0x42, 0x39, 0x50, 0x38, 0x55, 0x6c, 0x4a, 0x4a, 0x33, 0x48, 0x53, 0x45, 0x4c, 0x31, 0x64, 0x4a, 0x45, 0x35, 0x56, 0x50, 0x4c, 0x4d, 0x53, 0x6f, 0x77, 0x41, 0x6d, 0x78, 0x1e, 0x78, 0x59, 0x34, 0x65, 0x46, 0x6b, 0x54, 0x72, 0x33, 0x4d, 0x43, 0x79, 0x51, 0x67, 0x68, 0x59, 0x48, 0x5a, 0x36, 0x57, 0x62, 0x38, 0x4a, 0x64, 0x5f, 0x4b, 0x58, 0x62, 0x4c, 0x5f, 0xa5, 0x61, 0x56, 0x6c, 0x72, 0x48, 0x50, 0x55, 0x4c, 0x70, 0x71, 0x78, 0x31, 0x4b, 0x66, 0x6a, 0x61, 0x42, 0x65, 0x44, 0x35, 0x39, 0x7a, 0x68, 0x68, 0x48, 0x4c, 0x75, 0x45, 0x36, 0x6c, 0x46, 0x69, 0x6a, 0x79, 0x4d, 0x4e, 0x46, 0x41, 0x58, 0x71, 0x35, 0x39, 0x74, 0x68, 0x67, 0x62, 0x65, 0x70, 0x4f, 0x59, 0x33, 0x70, 0x67, 0x4d, 0x75, 0x59, 0x30, 0x73, 0x47, 0x66, 0x61, 0x49, 0x64, 0x41, 0x64, 0x36, 0x39, 0x64, 0x6d, 0x73, 0x77, 0x75, 0x78, 0x4b, 0x31, 0x4b, 0x58, 0x72, 0x79, 0x73, 0x4f, 0x37, 0x59, 0x45, 0x71, 0x48, 0x49, 0x44, 0x4b, 0x45, 0x49, 0x35, 0x7a, 0x48, 0x66, 0x55, 0x74, 0x64, 0x67, 0x34, 0x6d, 0x68, 0x67, 0x68, 0x33, 0x6c, 0x56, 0x6b, 0x71, 0x56, 0x53, 0x76, 0x32, 0x38, 0x33, 0x7a, 0x5a, 0x36, 0x56, 0x5f, 0x77, 0x75, 0x68, 0x36, 0x46, 0x67, 0x33, 0x69, 0x4f, 0x38, 0x72, 0x66, 0x73, 0x53, 0x33, 0x71, 0x57, 0x64, 0x34, 0x65, 0x74, 0x74, 0x38, 0x48, 0x51, 0x39, 0x63, 0x43, 0x30, 0x75, 0x49, 0x35, 0x63, 0x36, 0x38, 0x6e, 0x4e, 0x65, 0x30, 0x4f, 0x49, 0x64, 0x55, 0x49, 0x4b, 0x37, 0x69, 0x6b, 0x5a, 0x73, 0x48, 0x78, 0x1b, 0x6f, 0x32, 0x6e, 0x67, 0x34, 0x4b, 0x6c, 0x4a, 0x72, 0x64, 0x73, 0x69, 0x79, 0x56, 0x4b, 0x4b, 0x4b, 0x70, 0x73, 0x31, 0x4a, 0x64, 0x56, 0x61, 0x4d, 0x31, 0x67, 0x78, 0x27, 0x47, 0x54, 0x48, 0x47, 0x74, 0x47, 0x36, 0x4b, 0x31, 0x32, 0x56, 0x51, 0x49, 0x46, 0x59, 0x32, 0x67, 0x63, 0x31, 0x5f, 0x35, 0x4d, 0x52, 0x4a, 0x39, 0x50, 0x33, 0x5a, 0x71, 0x4f, 0x6c, 0x6d, 0x55, 0x39, 0x34, 0x48, 0x68, 0x75, 0x69, 0x78, 0x31, 0x30, 0x74, 0x6f, 0x72, 0x37, 0x41, 0x62, 0x58, 0x4f, 0x66, 0x75, 0x33, 0x61, 0x39, 0x6b, 0x67, 0x34, 0x75, 0x4b, 0x77, 0x44, 0x31, 0x4e, 0x62, 0x72, 0x43, 0x61, 0x4a, 0x32, 0x69, 0x55, 0x47, 0x4e, 0x57, 0x58, 0x66, 0x69, 0x41, 0x7a, 0x70, 0x5a, 0x34, 0x4b, 0x63, 0x39, 0x58, 0x4e, 0x34, 0x6c, 0x66, 0x4a, 0x4f, 0x4c, 0x41, 0x74, 0x31, 0xa3, 0x61, 0x75, 0x65, 0x41, 0x4f, 0x6f, 0x50, 0x57, 0x64, 0x59, 0x6d, 0x69, 0x53, 0x6b, 0x51, 0x58, 0x52, 0x6d, 0x69, 0x67, 0x6c, 0x70, 0x5a, 0x57, 0x49, 0x65, 0x56, 0x6a, 0x51, 0x33, 0x73, 0x64, 0x4e, 0x4c, 0x38, 0x58, 0x65, 0x5f, 0x79, 0x73, 0x62, 0x58, 0xa3, 0x61, 0x69, 0x68, 0x34, 0x69, 0x6c, 0x71, 0x54, 0x30, 0x67, 0x78, 0x63, 0x72, 0x68, 0x55, 0x66, 0x6f, 0x65, 0x75, 0x63, 0x49, 0x65, 0x62, 0x6f, 0x36, 0x63, 0x58, 0x39, 0x63, 0x6f, 0x63, 0x6c, 0x74, 0x37, 0x71, 0x72, 0x78, 0x43, 0x33, 0x57, 0x50, 0x55, 0x30, 0x75, 0x61, 0x77, 0x58, 0x38, 0x75, 0x46, 0x61, 0x33, 0x65, 0x39, 0x63, 0x70, 0x43, 0x69, 0x6e, 0x50, 0x37, 0x37, 0x75, 0x58, 0x5a, 0x51, 0x79, 0x52, 0x67 }; + static_assert(sizeof(cborData) == 16384); + // FIXME: Intermediate buffer is used because exflash_hal requires the source buffer to reside in RAM + char buf[256] = {}; + size_t offs = 0; + while (offs < sizeof(cborData)) { + size_t n = std::min(sizeof(buf), sizeof(cborData) - offs); + std::memcpy(buf, cborData + offs, n); + assertEqual(ledger_write(stream, buf, n, nullptr /* reserved */), (int)n); + offs += n; + } + // Register a sync callback + g_synced = false; + ledger_callbacks callbacks = { .version = LEDGER_API_VERSION }; + callbacks.sync = [](ledger_instance* /* ledger */, void* /* arg */) { + g_synced = true; + }; + ledger_set_callbacks(ledger, &callbacks, nullptr /* reserved */); + SCOPE_GUARD({ + ledger_set_callbacks(ledger, nullptr /* callbacks */, nullptr /* reserved */); + }); + // Close the stream and wait until the ledger is synchronized + closeStreamGuard.dismiss(); + assertEqual(ledger_close(stream, 0 /* flags */, nullptr /* reserved */), 0); + waitFor([]() { + return g_synced; + }, 90000); + assertTrue(g_synced); +} + +test(06_update_cloud_to_device_large_size) { + ledger_instance* ledger = nullptr; + assertEqual(ledger_get_instance(&ledger, CLOUD_TO_DEVICE_LEDGER, nullptr /* reserved */), 0); + // Register a sync callback + g_synced = false; + ledger_callbacks callbacks = { .version = LEDGER_API_VERSION }; + callbacks.sync = [](ledger_instance* /* ledger */, void* /* arg */) { + g_synced = true; + }; + ledger_set_callbacks(ledger, &callbacks, nullptr /* reserved */); + // Do not release the instance to keep the callback registered for the next test +} + +test(07_validate_cloud_to_device_sync_large_size) { + waitFor([]() { + return g_synced; + }, 90000); + // Use the system API to avoid allocating a lot of RAM + ledger_instance* ledger = nullptr; + assertEqual(ledger_get_instance(&ledger, CLOUD_TO_DEVICE_LEDGER, nullptr /* reserved */), 0); + SCOPE_GUARD({ + ledger_release(ledger, nullptr /* reserved */); + }); + ledger_set_callbacks(ledger, nullptr /* callbacks */, nullptr /* reserved */); + ledger_release(ledger, nullptr /* reserved */); // Acquired in the previous test + assertTrue(g_synced); + // Validate the ledger data + ledger_stream* stream = nullptr; + assertEqual(ledger_open(&stream, ledger, LEDGER_STREAM_MODE_READ, nullptr /* reserved */), 0); + NAMED_SCOPE_GUARD(closeStreamGuard, { + ledger_close(stream, 0 /* flags */, nullptr /* reserved */); + }); + static const uint8_t expectedCborData[] = { 0xb8, 0xae, 0x62, 0x34, 0x36, 0x78, 0x27, 0x4f, 0x42, 0x54, 0x7a, 0x59, 0x6e, 0x50, 0x32, 0x7a, 0x78, 0x50, 0x4a, 0x32, 0x39, 0x4f, 0x4f, 0x4b, 0x4d, 0x63, 0x59, 0x38, 0x5a, 0x32, 0x32, 0x7a, 0x41, 0x74, 0x36, 0x33, 0x32, 0x4b, 0x56, 0x73, 0x45, 0x6d, 0x47, 0x67, 0x57, 0x71, 0x62, 0x36, 0x39, 0x78, 0x54, 0x33, 0x6b, 0x70, 0x5f, 0x30, 0x7a, 0x79, 0x58, 0x6d, 0x33, 0x74, 0x6f, 0x72, 0x43, 0x65, 0x4a, 0x61, 0x30, 0x49, 0x72, 0x51, 0x75, 0x76, 0x70, 0x70, 0x7a, 0x4f, 0x4e, 0x4d, 0x34, 0x37, 0x75, 0x51, 0x53, 0x4d, 0x70, 0x4c, 0x50, 0x6e, 0x75, 0x44, 0x6d, 0x6f, 0x5a, 0x70, 0x37, 0x36, 0x6a, 0x53, 0x41, 0x69, 0x64, 0x53, 0x74, 0x69, 0x4f, 0x34, 0x71, 0x30, 0x70, 0x6f, 0x6d, 0x42, 0x79, 0x44, 0x62, 0x4c, 0x67, 0x55, 0x37, 0x35, 0x41, 0x44, 0x74, 0x41, 0x56, 0x69, 0x74, 0x46, 0x47, 0x58, 0x5f, 0x4b, 0x6d, 0x61, 0x4f, 0x78, 0x31, 0x62, 0x4d, 0x7a, 0x55, 0x6e, 0x59, 0x58, 0x34, 0x4e, 0x6d, 0x30, 0x4d, 0x36, 0x68, 0x58, 0x67, 0x44, 0x64, 0x62, 0x79, 0x5a, 0x79, 0x71, 0x58, 0x5f, 0x72, 0x4d, 0x63, 0x69, 0x6e, 0x47, 0x68, 0x48, 0x58, 0x6b, 0x69, 0x52, 0x79, 0x38, 0x31, 0x55, 0x73, 0x49, 0x31, 0x4d, 0x41, 0x52, 0x38, 0x47, 0x78, 0x31, 0x32, 0x4b, 0x41, 0x71, 0x42, 0x61, 0x38, 0x6d, 0x4f, 0x53, 0x31, 0x42, 0x56, 0x6b, 0x6c, 0x77, 0x31, 0x73, 0x37, 0x37, 0x57, 0x41, 0x4d, 0x45, 0x38, 0x48, 0x32, 0x4c, 0x4f, 0x35, 0x32, 0x43, 0x45, 0x58, 0x41, 0x51, 0x51, 0x31, 0x65, 0x39, 0x41, 0x33, 0x6a, 0x42, 0x64, 0x44, 0x32, 0x46, 0x76, 0x78, 0x46, 0x79, 0x6f, 0x39, 0x46, 0x71, 0x31, 0x5a, 0x44, 0x78, 0x74, 0x6e, 0x49, 0x43, 0x52, 0x65, 0x69, 0x4f, 0x4a, 0x47, 0x43, 0x71, 0x64, 0x63, 0x53, 0x77, 0x62, 0x6a, 0x6e, 0x55, 0x52, 0x4f, 0x37, 0x38, 0x73, 0x36, 0x52, 0x35, 0x52, 0x72, 0x5f, 0x55, 0x32, 0x4c, 0x78, 0x35, 0x4a, 0x51, 0x33, 0x41, 0x69, 0x72, 0x72, 0x42, 0x63, 0x4b, 0x76, 0x67, 0x64, 0x35, 0x76, 0x6b, 0x50, 0x51, 0x58, 0x52, 0x71, 0x44, 0x6b, 0x66, 0x43, 0x74, 0x47, 0x55, 0x44, 0x61, 0x44, 0x35, 0x37, 0x44, 0x55, 0x48, 0x48, 0x33, 0x62, 0x79, 0x6d, 0x34, 0x79, 0x30, 0x52, 0x51, 0xa2, 0x61, 0x46, 0x71, 0x44, 0x72, 0x30, 0x52, 0x74, 0x50, 0x50, 0x39, 0x31, 0x30, 0x64, 0x56, 0x79, 0x35, 0x6f, 0x59, 0x79, 0x64, 0x31, 0x4b, 0x51, 0x74, 0x63, 0x74, 0x4f, 0x47, 0x78, 0x2d, 0x56, 0x63, 0x45, 0x48, 0x33, 0x37, 0x4c, 0x55, 0x6b, 0x67, 0x4a, 0x46, 0x79, 0x4f, 0x50, 0x37, 0x4c, 0x75, 0x4f, 0x75, 0x4a, 0x39, 0x31, 0x4f, 0x6b, 0x68, 0x6b, 0x44, 0x50, 0x48, 0x51, 0x34, 0x55, 0x36, 0x4e, 0x50, 0x4b, 0x52, 0x70, 0x70, 0x53, 0x4b, 0x68, 0x45, 0x4f, 0xa6, 0x61, 0x37, 0x61, 0x49, 0x63, 0x74, 0x6e, 0x7a, 0x70, 0x66, 0x39, 0x6f, 0x75, 0x53, 0x6c, 0x54, 0x55, 0x47, 0x57, 0x6f, 0x74, 0x6b, 0x57, 0x56, 0x30, 0x62, 0x44, 0x4c, 0x6b, 0x6a, 0x4a, 0x4a, 0x72, 0x6e, 0x51, 0x52, 0x56, 0x4f, 0x46, 0x33, 0x62, 0x7a, 0x39, 0x63, 0x66, 0x73, 0x49, 0x62, 0x30, 0x46, 0x70, 0x65, 0x46, 0x48, 0x61, 0x73, 0x32, 0x67, 0x59, 0x72, 0x37, 0x6a, 0x37, 0x78, 0x69, 0x72, 0x50, 0x63, 0x48, 0x47, 0x36, 0x62, 0x46, 0x6d, 0x6a, 0x70, 0x34, 0x35, 0x44, 0x75, 0x4f, 0x57, 0x34, 0x75, 0x55, 0xa2, 0x61, 0x36, 0x68, 0x4f, 0x73, 0x41, 0x7a, 0x4e, 0x5a, 0x4d, 0x53, 0x61, 0x43, 0x65, 0x30, 0x7a, 0x5f, 0x46, 0x30, 0x66, 0x44, 0x75, 0x6a, 0x45, 0x64, 0x68, 0x78, 0x4e, 0x6e, 0x76, 0x64, 0x75, 0x37, 0x64, 0x6e, 0x69, 0x44, 0x64, 0x56, 0x36, 0x55, 0x4f, 0x75, 0x51, 0x72, 0x67, 0x7a, 0x78, 0x4c, 0x68, 0x66, 0x61, 0x59, 0x73, 0x4c, 0x30, 0x42, 0x67, 0x66, 0x68, 0x50, 0x44, 0x48, 0x5a, 0x62, 0x61, 0x43, 0x74, 0x47, 0x37, 0x6e, 0x73, 0x30, 0x48, 0x50, 0x59, 0x65, 0x44, 0x69, 0x4e, 0x75, 0x6b, 0x48, 0x55, 0x41, 0x36, 0x55, 0x4b, 0x76, 0x6f, 0x56, 0x6d, 0x57, 0x4e, 0x66, 0x6f, 0x6a, 0x50, 0x66, 0x76, 0x59, 0x69, 0x75, 0x4c, 0x76, 0x32, 0x78, 0x22, 0x4d, 0x77, 0x57, 0x6f, 0x77, 0x61, 0x6b, 0x79, 0x75, 0x4a, 0x31, 0x69, 0x4c, 0x46, 0x4e, 0x58, 0x51, 0x44, 0x64, 0x4f, 0x70, 0x43, 0x59, 0x63, 0x36, 0x6d, 0x50, 0x4b, 0x78, 0x68, 0x50, 0x59, 0x4e, 0x79, 0x78, 0x4e, 0x48, 0x61, 0x68, 0x77, 0x66, 0x39, 0x6f, 0x78, 0x6f, 0x7a, 0x37, 0x58, 0x6a, 0x31, 0x39, 0x67, 0x4d, 0x44, 0x6f, 0x56, 0x57, 0x43, 0x52, 0x4c, 0x57, 0x4e, 0x77, 0x77, 0x6a, 0x71, 0x6c, 0x41, 0x7a, 0x48, 0x6f, 0x42, 0x6d, 0x6a, 0x34, 0x72, 0x78, 0x48, 0x49, 0x65, 0x73, 0x30, 0x34, 0x4f, 0x49, 0x47, 0x35, 0x5f, 0x66, 0x6f, 0x58, 0x47, 0x78, 0x66, 0x59, 0x6c, 0x78, 0x39, 0x42, 0x75, 0x46, 0x5a, 0x59, 0x41, 0x68, 0x4a, 0x4e, 0x52, 0x49, 0x30, 0x48, 0x31, 0x56, 0x51, 0x78, 0x27, 0x7a, 0x34, 0x57, 0x59, 0x6a, 0x42, 0x78, 0x32, 0x64, 0x69, 0x52, 0x74, 0x67, 0x4a, 0x55, 0x76, 0x6a, 0x74, 0x50, 0x4e, 0x74, 0x46, 0x6e, 0x77, 0x33, 0x67, 0x54, 0x52, 0x68, 0x48, 0x61, 0x43, 0x73, 0x73, 0x30, 0x44, 0x44, 0x6a, 0x36, 0xa2, 0x61, 0x41, 0x71, 0x30, 0x65, 0x54, 0x76, 0x78, 0x45, 0x6b, 0x66, 0x58, 0x58, 0x76, 0x31, 0x52, 0x70, 0x59, 0x4e, 0x48, 0x64, 0x78, 0x32, 0x66, 0x74, 0x6b, 0x50, 0x62, 0x62, 0x58, 0x66, 0x31, 0x30, 0x7a, 0x70, 0x52, 0x69, 0x65, 0x75, 0x79, 0x69, 0x4c, 0x62, 0x78, 0x36, 0x6f, 0x6b, 0x69, 0x6e, 0x4a, 0x33, 0x4f, 0x74, 0x7a, 0x44, 0x38, 0x50, 0x55, 0x63, 0x71, 0x6f, 0x6a, 0x4c, 0x38, 0x57, 0x72, 0x35, 0x55, 0x54, 0x4f, 0x63, 0x49, 0x33, 0x48, 0x4d, 0x51, 0x4b, 0x64, 0x5a, 0x66, 0x6c, 0x74, 0x4c, 0x48, 0x77, 0x62, 0x52, 0x72, 0x6c, 0x57, 0x63, 0x48, 0x4b, 0x41, 0x38, 0x42, 0x38, 0x4c, 0x33, 0x78, 0x26, 0x69, 0x49, 0x6c, 0x45, 0x42, 0x79, 0x53, 0x5a, 0x57, 0x7a, 0x52, 0x47, 0x78, 0x5f, 0x68, 0x6b, 0x52, 0x39, 0x42, 0x4a, 0x61, 0x64, 0x4b, 0x50, 0x45, 0x38, 0x57, 0x4c, 0x33, 0x45, 0x4a, 0x4b, 0x57, 0x77, 0x55, 0x38, 0x65, 0x32, 0x78, 0x49, 0x58, 0x35, 0x6c, 0x6f, 0x57, 0x73, 0x37, 0x36, 0x71, 0x51, 0x55, 0x70, 0x47, 0x4e, 0x78, 0x35, 0x6c, 0x78, 0x48, 0x4c, 0x30, 0x63, 0x6e, 0x62, 0x46, 0x73, 0x7a, 0x41, 0x5a, 0x74, 0x72, 0x32, 0x57, 0x75, 0x36, 0x6d, 0x39, 0x30, 0x55, 0x34, 0x5a, 0x43, 0x54, 0x41, 0x37, 0x45, 0x79, 0x68, 0x74, 0x69, 0x68, 0x75, 0x74, 0x56, 0x73, 0x32, 0x6b, 0x32, 0x5f, 0x5a, 0x57, 0x34, 0x74, 0x56, 0x61, 0x4f, 0x64, 0x57, 0x57, 0x6d, 0x6a, 0x5f, 0x62, 0x62, 0x47, 0x4e, 0xa8, 0x61, 0x36, 0x67, 0x63, 0x36, 0x68, 0x6b, 0x67, 0x6f, 0x6d, 0x61, 0x45, 0x65, 0x47, 0x41, 0x41, 0x57, 0x35, 0x66, 0x68, 0x6c, 0x7a, 0x33, 0x74, 0x6c, 0x68, 0x48, 0x78, 0x73, 0x4e, 0x62, 0x54, 0x7a, 0x75, 0x66, 0x75, 0x5a, 0x35, 0x49, 0x39, 0x55, 0x6b, 0x4c, 0x4d, 0x6d, 0x32, 0x45, 0x43, 0x56, 0x66, 0x74, 0x63, 0x4f, 0x64, 0x41, 0x65, 0x6f, 0x7a, 0x70, 0x50, 0x59, 0x44, 0x76, 0x6c, 0x37, 0x5f, 0x47, 0x65, 0x43, 0x4c, 0x6e, 0x62, 0x66, 0x69, 0x57, 0x62, 0x69, 0x51, 0x64, 0x50, 0x71, 0x58, 0x66, 0x68, 0x39, 0x49, 0x69, 0x65, 0x46, 0x7a, 0x5a, 0x6f, 0x69, 0x6b, 0x69, 0x79, 0x36, 0x72, 0x32, 0x4c, 0x43, 0x64, 0x63, 0x76, 0x30, 0x6e, 0x66, 0x5f, 0x5a, 0x63, 0x51, 0x37, 0x57, 0x67, 0x36, 0x68, 0x55, 0x69, 0x62, 0x65, 0x5a, 0x78, 0x20, 0x57, 0x42, 0x73, 0x73, 0x6c, 0x70, 0x39, 0x58, 0x69, 0x72, 0x50, 0x50, 0x55, 0x57, 0x46, 0x42, 0x66, 0x4a, 0x79, 0x49, 0x69, 0x53, 0x45, 0x76, 0x46, 0x4c, 0x74, 0x4a, 0x72, 0x4b, 0x6d, 0x71, 0x61, 0x54, 0x78, 0x67, 0x33, 0x63, 0x66, 0x38, 0x36, 0x5f, 0x37, 0x76, 0x74, 0x66, 0x44, 0x51, 0x39, 0x39, 0x74, 0x7a, 0x65, 0x46, 0x4c, 0x44, 0x78, 0x44, 0x30, 0x35, 0x78, 0x71, 0x70, 0x35, 0x7a, 0x32, 0x4b, 0x71, 0x53, 0x67, 0x4d, 0x57, 0x36, 0x6c, 0x65, 0x44, 0x78, 0x37, 0x54, 0x6a, 0x4b, 0x69, 0x69, 0x6a, 0x34, 0x30, 0x4f, 0x30, 0x4a, 0x79, 0x68, 0x5a, 0x52, 0x35, 0x58, 0x68, 0x51, 0x69, 0x48, 0x53, 0x71, 0x5f, 0x56, 0x77, 0x49, 0x76, 0x48, 0x43, 0x4b, 0x4b, 0x74, 0x73, 0x78, 0x5a, 0x30, 0x46, 0x52, 0x31, 0x67, 0x7a, 0x33, 0x50, 0x51, 0x62, 0x78, 0x63, 0x46, 0x43, 0x31, 0x74, 0x68, 0x58, 0x4b, 0x5a, 0x64, 0x72, 0x42, 0x41, 0x35, 0x78, 0x28, 0x52, 0x37, 0x56, 0x6c, 0x71, 0x4a, 0x57, 0x61, 0x58, 0x32, 0x78, 0x74, 0x65, 0x63, 0x34, 0x45, 0x49, 0x36, 0x64, 0x63, 0x6a, 0x4a, 0x5a, 0x54, 0x5f, 0x6f, 0x63, 0x43, 0x69, 0x62, 0x39, 0x38, 0x45, 0x6a, 0x37, 0x78, 0x53, 0x54, 0x6e, 0x36, 0x78, 0x4d, 0x46, 0x46, 0x78, 0x75, 0x4f, 0x59, 0x70, 0x4a, 0x6c, 0x6c, 0x6f, 0x6d, 0x6d, 0x46, 0x43, 0x76, 0x35, 0x33, 0x79, 0x66, 0x51, 0x32, 0x69, 0x30, 0x69, 0x70, 0x4b, 0x76, 0x4c, 0x43, 0x48, 0x72, 0x37, 0x47, 0x6c, 0x57, 0x4a, 0x6e, 0x45, 0x34, 0x51, 0x4d, 0x36, 0x33, 0x38, 0x73, 0x39, 0x67, 0x57, 0x32, 0x5a, 0x57, 0x36, 0x5a, 0x54, 0x4e, 0x79, 0x6e, 0x69, 0x5a, 0x75, 0x45, 0x72, 0x38, 0x35, 0x35, 0x7a, 0x4a, 0x53, 0x6c, 0x49, 0x69, 0x74, 0x42, 0x42, 0x55, 0x31, 0x62, 0x63, 0x41, 0x78, 0x73, 0x69, 0x34, 0x7a, 0x41, 0x4b, 0x52, 0x59, 0x46, 0x6c, 0x7a, 0x37, 0x73, 0x51, 0x32, 0x73, 0x44, 0x70, 0x38, 0x46, 0x74, 0x42, 0x55, 0x64, 0x59, 0x58, 0x57, 0x4c, 0x38, 0x6b, 0x71, 0x54, 0x6e, 0x6e, 0x50, 0x4b, 0x65, 0x46, 0x73, 0x54, 0x54, 0x67, 0x79, 0x37, 0x5a, 0x50, 0x46, 0x72, 0x49, 0x6b, 0x46, 0x49, 0x64, 0x4e, 0x38, 0x30, 0x35, 0x70, 0x77, 0x6a, 0x31, 0x5f, 0x52, 0x41, 0x6b, 0x66, 0x62, 0x77, 0x66, 0x54, 0x42, 0x46, 0x44, 0x7a, 0x44, 0x68, 0x44, 0x39, 0x38, 0x69, 0x45, 0x4e, 0x57, 0x44, 0x4e, 0x58, 0x36, 0x45, 0x68, 0x43, 0x56, 0x7a, 0x49, 0x64, 0x55, 0x56, 0x79, 0x71, 0x56, 0x54, 0x74, 0x33, 0x42, 0x7a, 0x36, 0x53, 0x38, 0x69, 0x52, 0x67, 0x38, 0x50, 0x45, 0x65, 0x42, 0x36, 0x6d, 0x64, 0x6d, 0x64, 0x67, 0x38, 0x69, 0x78, 0x31, 0x7a, 0x44, 0x57, 0x39, 0x35, 0x78, 0x41, 0x61, 0x54, 0x6b, 0x6e, 0x6f, 0x79, 0x58, 0x7a, 0x43, 0x41, 0x50, 0x51, 0x79, 0x70, 0x62, 0x79, 0x58, 0x65, 0x37, 0x43, 0x55, 0x6e, 0x62, 0x46, 0x6c, 0x41, 0x6d, 0x59, 0x38, 0x66, 0x6e, 0x76, 0x47, 0x6b, 0x74, 0x47, 0x52, 0x56, 0x35, 0x5a, 0x5a, 0x58, 0x57, 0x61, 0x65, 0x63, 0x51, 0x4c, 0x67, 0x42, 0x56, 0x66, 0x70, 0x75, 0x4f, 0x7a, 0x5a, 0x47, 0x45, 0x57, 0x68, 0x45, 0x75, 0x78, 0x33, 0x69, 0x64, 0x70, 0x57, 0x58, 0x41, 0x6f, 0x4b, 0x64, 0x73, 0x78, 0x1d, 0x72, 0x50, 0x49, 0x69, 0x6d, 0x72, 0x57, 0x57, 0x78, 0x4c, 0x65, 0x64, 0x31, 0x76, 0x55, 0x6c, 0x37, 0x71, 0x4f, 0x73, 0x51, 0x56, 0x65, 0x30, 0x6a, 0x43, 0x64, 0x6f, 0x7a, 0x78, 0x1a, 0x65, 0x59, 0x6a, 0x49, 0x30, 0x5f, 0x7a, 0x52, 0x65, 0x67, 0x4b, 0x56, 0x4d, 0x6a, 0x78, 0x76, 0x74, 0x4d, 0x37, 0x68, 0x6f, 0x31, 0x36, 0x76, 0x35, 0x37, 0x78, 0x4a, 0x56, 0x36, 0x65, 0x79, 0x45, 0x45, 0x66, 0x4f, 0x45, 0x6d, 0x6c, 0x78, 0x4c, 0x39, 0x66, 0x54, 0x35, 0x48, 0x61, 0x6f, 0x53, 0x4c, 0x53, 0x39, 0x62, 0x77, 0x74, 0x42, 0x63, 0x39, 0x6f, 0x54, 0x76, 0x54, 0x67, 0x73, 0x51, 0x47, 0x79, 0x61, 0x54, 0x46, 0x44, 0x5f, 0x30, 0x5a, 0x37, 0x6a, 0x66, 0x47, 0x6a, 0x5f, 0x5a, 0x46, 0x77, 0x79, 0x75, 0x78, 0x52, 0x4a, 0x77, 0x54, 0x6e, 0x72, 0x44, 0x6c, 0x33, 0x32, 0x59, 0x6c, 0x4f, 0x44, 0x45, 0x57, 0x62, 0x59, 0x32, 0x78, 0x38, 0x59, 0x5a, 0x4f, 0x66, 0x4c, 0x67, 0x66, 0x71, 0x78, 0x7a, 0x4d, 0x4d, 0x74, 0x67, 0x59, 0x4d, 0x6a, 0x76, 0x61, 0x50, 0x55, 0x52, 0x71, 0x34, 0x4f, 0x6d, 0x78, 0x39, 0x63, 0x5a, 0x63, 0x59, 0x5a, 0x34, 0x30, 0x34, 0x64, 0x59, 0x5a, 0x70, 0x49, 0x51, 0x48, 0x43, 0x77, 0x4e, 0x38, 0x6f, 0x72, 0x56, 0x46, 0x5a, 0x68, 0x6b, 0x4d, 0x69, 0x6e, 0x67, 0x67, 0x43, 0x4b, 0x65, 0x43, 0x69, 0x4f, 0x44, 0x64, 0x57, 0x37, 0x49, 0x50, 0x77, 0x5a, 0x67, 0x5a, 0x30, 0x76, 0x73, 0x41, 0x30, 0x74, 0x6a, 0x5f, 0x36, 0x32, 0x71, 0x6a, 0x66, 0x52, 0x61, 0x75, 0x4a, 0x45, 0x78, 0x4c, 0x67, 0x6f, 0x68, 0x38, 0x4b, 0x48, 0x49, 0x46, 0x78, 0x23, 0x49, 0x6d, 0x6b, 0x42, 0x46, 0x4e, 0x57, 0x42, 0x5a, 0x64, 0x31, 0x6a, 0x76, 0x64, 0x6d, 0x59, 0x4c, 0x5a, 0x64, 0x65, 0x4b, 0x75, 0x4c, 0x77, 0x61, 0x69, 0x6e, 0x35, 0x6c, 0x53, 0x53, 0x41, 0x4d, 0x46, 0x41, 0x78, 0x1f, 0x78, 0x4e, 0x37, 0x6b, 0x66, 0x53, 0x64, 0x73, 0x4a, 0x66, 0x65, 0x64, 0x75, 0x38, 0x70, 0x49, 0x48, 0x48, 0x55, 0x4c, 0x62, 0x45, 0x67, 0x33, 0x74, 0x4c, 0x6a, 0x43, 0x38, 0x4f, 0x67, 0x78, 0x54, 0x61, 0x56, 0x49, 0x4b, 0x37, 0x30, 0x52, 0x37, 0x56, 0x71, 0x70, 0x43, 0x66, 0x42, 0x35, 0x57, 0x7a, 0x49, 0x5a, 0x35, 0x35, 0x78, 0x61, 0x48, 0x77, 0x4a, 0x46, 0x4c, 0x72, 0x77, 0x64, 0x59, 0x75, 0x69, 0x39, 0x51, 0x54, 0x6c, 0x78, 0x67, 0x48, 0x61, 0x72, 0x57, 0x4d, 0x48, 0x37, 0x74, 0x49, 0x57, 0x77, 0x49, 0x59, 0x4d, 0x47, 0x61, 0x68, 0x4a, 0x59, 0x5f, 0x78, 0x4b, 0x43, 0x37, 0x4f, 0x36, 0x79, 0x75, 0x5a, 0x56, 0x4a, 0x64, 0x4c, 0x33, 0x63, 0x74, 0x7a, 0x4d, 0x31, 0x38, 0x65, 0x67, 0x79, 0x48, 0x6d, 0x38, 0x67, 0x45, 0x6e, 0x46, 0x7a, 0x69, 0x50, 0x65, 0x50, 0x41, 0x32, 0x38, 0x74, 0x37, 0x37, 0x43, 0x30, 0x62, 0x78, 0x7a, 0x36, 0x4b, 0x56, 0x57, 0x61, 0x66, 0x37, 0x63, 0x52, 0x77, 0x65, 0x6d, 0x43, 0x78, 0x1a, 0x30, 0x57, 0x59, 0x48, 0x5a, 0x44, 0x76, 0x6f, 0x49, 0x70, 0x53, 0x66, 0x56, 0x6c, 0x55, 0x68, 0x69, 0x33, 0x30, 0x72, 0x73, 0x79, 0x43, 0x68, 0x71, 0x72, 0x78, 0x37, 0x33, 0x36, 0x46, 0x4c, 0x4a, 0x32, 0x77, 0x32, 0x48, 0x67, 0x65, 0x50, 0x70, 0x31, 0x38, 0x33, 0x72, 0x52, 0x59, 0x51, 0x4c, 0x5f, 0x65, 0x55, 0x59, 0x41, 0x59, 0x45, 0x4e, 0x56, 0x6b, 0x31, 0x68, 0x79, 0x68, 0x4f, 0x46, 0x76, 0x51, 0x65, 0x39, 0x45, 0x56, 0x6b, 0x5a, 0x4c, 0x75, 0x49, 0x65, 0x32, 0x43, 0x6d, 0x70, 0x59, 0x46, 0x6f, 0x6e, 0x58, 0x74, 0x43, 0x74, 0x6e, 0x39, 0x6f, 0x30, 0x4b, 0x62, 0x70, 0x46, 0x55, 0x46, 0x78, 0x1b, 0x6f, 0x4b, 0x67, 0x59, 0x62, 0x5a, 0x62, 0x49, 0x62, 0x39, 0x42, 0x42, 0x6b, 0x76, 0x31, 0x6a, 0x46, 0x73, 0x48, 0x55, 0x4e, 0x50, 0x44, 0x41, 0x76, 0x75, 0x66, 0x72, 0x51, 0x37, 0x31, 0x5a, 0x66, 0x65, 0x6e, 0x4e, 0x33, 0x36, 0x77, 0x69, 0x58, 0x5f, 0x57, 0x41, 0x76, 0x50, 0x78, 0x57, 0x32, 0x6a, 0x6a, 0x30, 0x6d, 0x63, 0x5a, 0x49, 0x64, 0x61, 0x48, 0x35, 0x58, 0x56, 0x44, 0x79, 0x4b, 0x36, 0x74, 0x33, 0x54, 0x47, 0x72, 0x66, 0x59, 0x74, 0x71, 0x62, 0x53, 0x76, 0x42, 0x73, 0x43, 0x75, 0x67, 0x46, 0x35, 0x4a, 0x33, 0x53, 0x71, 0x53, 0x70, 0x63, 0x37, 0x34, 0x57, 0x70, 0x34, 0x70, 0x65, 0x78, 0x62, 0x33, 0x67, 0x4c, 0x69, 0x6a, 0x35, 0x59, 0x42, 0x6d, 0x6d, 0x32, 0x6c, 0x49, 0x30, 0x6f, 0x59, 0x45, 0x37, 0x55, 0x63, 0x6f, 0x77, 0x58, 0x63, 0x79, 0x45, 0x33, 0x38, 0x5a, 0x54, 0x56, 0x68, 0x58, 0x59, 0x78, 0x28, 0x31, 0x44, 0x64, 0x48, 0x47, 0x41, 0x6c, 0x43, 0x4f, 0x4d, 0x4f, 0x6c, 0x4a, 0x79, 0x74, 0x46, 0x63, 0x45, 0x51, 0x35, 0x71, 0x54, 0x43, 0x75, 0x31, 0x37, 0x68, 0x76, 0x44, 0x62, 0x77, 0x76, 0x38, 0x7a, 0x34, 0x5a, 0x36, 0x4d, 0x59, 0x49, 0xa5, 0x61, 0x69, 0x6d, 0x49, 0x71, 0x53, 0x6c, 0x42, 0x47, 0x6a, 0x35, 0x4c, 0x47, 0x34, 0x49, 0x43, 0x62, 0x4d, 0x32, 0x70, 0x34, 0x6f, 0x47, 0x33, 0x4a, 0x64, 0x47, 0x68, 0x45, 0x61, 0x66, 0x4b, 0x6c, 0x37, 0x65, 0x6d, 0x61, 0x4f, 0x64, 0x75, 0x78, 0x74, 0x6e, 0x65, 0x5a, 0x43, 0x6c, 0x72, 0x76, 0x64, 0x65, 0x59, 0x75, 0x37, 0x63, 0x52, 0x41, 0x6d, 0x64, 0x65, 0x53, 0x30, 0x36, 0x78, 0x27, 0x46, 0x30, 0x31, 0x36, 0x46, 0x77, 0x31, 0x41, 0x58, 0x66, 0x66, 0x64, 0x79, 0x34, 0x59, 0x67, 0x6a, 0x5a, 0x39, 0x53, 0x6a, 0x4a, 0x34, 0x6c, 0x4a, 0x72, 0x76, 0x38, 0x59, 0x4d, 0x34, 0x4f, 0x44, 0x65, 0x63, 0x4c, 0x57, 0x67, 0x34, 0xa4, 0x61, 0x6c, 0x65, 0x5f, 0x48, 0x6d, 0x44, 0x36, 0x61, 0x66, 0x69, 0x69, 0x36, 0x57, 0x63, 0x6b, 0x32, 0x4e, 0x78, 0x4c, 0x62, 0x50, 0x6a, 0x66, 0x47, 0x61, 0x33, 0x42, 0x55, 0x67, 0x65, 0x34, 0x69, 0x50, 0x77, 0x5a, 0x64, 0x35, 0x47, 0x65, 0x72, 0x78, 0x27, 0x31, 0x39, 0x4f, 0x78, 0x53, 0x75, 0x68, 0x51, 0x32, 0x31, 0x55, 0x6a, 0x52, 0x36, 0x49, 0x6d, 0x52, 0x46, 0x4a, 0x53, 0x6a, 0x48, 0x5a, 0x6b, 0x78, 0x45, 0x31, 0x73, 0x37, 0x75, 0x6f, 0x6e, 0x57, 0x35, 0x56, 0x70, 0x51, 0x61, 0x53, 0xa5, 0x61, 0x5a, 0x65, 0x6d, 0x4c, 0x39, 0x4c, 0x31, 0x63, 0x4c, 0x4c, 0x62, 0xa1, 0x61, 0x77, 0x63, 0x37, 0x41, 0x4f, 0x61, 0x64, 0x68, 0x55, 0x32, 0x67, 0x46, 0x72, 0x4b, 0x70, 0x4f, 0x61, 0x6d, 0x72, 0x75, 0x54, 0x72, 0x75, 0x6d, 0x67, 0x79, 0x36, 0x62, 0x51, 0x49, 0x34, 0x50, 0x39, 0x6b, 0x62, 0x44, 0x31, 0x63, 0x61, 0x78, 0x59, 0x64, 0x74, 0x52, 0x36, 0x72, 0x78, 0x21, 0x64, 0x50, 0x57, 0x4f, 0x55, 0x4a, 0x53, 0x44, 0x30, 0x66, 0x51, 0x43, 0x30, 0x37, 0x69, 0x51, 0x75, 0x38, 0x4b, 0x48, 0x64, 0x77, 0x4d, 0x4d, 0x6e, 0x7a, 0x7a, 0x51, 0x79, 0x56, 0x53, 0x4c, 0x56, 0x78, 0x2e, 0x4f, 0x44, 0x4d, 0x46, 0x76, 0x49, 0x56, 0x4d, 0x5a, 0x58, 0x78, 0x48, 0x4d, 0x56, 0x57, 0x4d, 0x68, 0x55, 0x75, 0x46, 0x6c, 0x69, 0x43, 0x57, 0x79, 0x35, 0x50, 0x54, 0x64, 0x59, 0x6c, 0x56, 0x59, 0x67, 0x65, 0x4c, 0x79, 0x5f, 0x75, 0x36, 0x51, 0x41, 0x50, 0x6b, 0x65, 0x72, 0x78, 0x28, 0x44, 0x5a, 0x56, 0x49, 0x42, 0x49, 0x71, 0x4f, 0x62, 0x42, 0x64, 0x51, 0x67, 0x35, 0x54, 0x32, 0x78, 0x35, 0x36, 0x6c, 0x6f, 0x39, 0x62, 0x48, 0x33, 0x4e, 0x46, 0x58, 0x6e, 0x68, 0x32, 0x77, 0x57, 0x66, 0x35, 0x50, 0x45, 0x32, 0x50, 0x61, 0x78, 0x31, 0x34, 0x63, 0x35, 0x76, 0x4b, 0x32, 0x7a, 0x31, 0x4e, 0x58, 0x34, 0x34, 0x70, 0x61, 0x52, 0x42, 0x48, 0x44, 0x4b, 0x4a, 0x57, 0x70, 0x6d, 0x44, 0x58, 0x62, 0x49, 0x73, 0x35, 0x41, 0x57, 0x6e, 0x38, 0x43, 0x4d, 0x34, 0x4f, 0x34, 0x41, 0x49, 0x5a, 0x6e, 0x53, 0x31, 0x32, 0x4a, 0x46, 0x6a, 0x32, 0x66, 0x6f, 0x75, 0x78, 0x38, 0x4f, 0x34, 0xa3, 0x61, 0x64, 0x75, 0x43, 0x78, 0x4b, 0x49, 0x32, 0x59, 0x56, 0x6c, 0x6f, 0x4c, 0x4e, 0x30, 0x65, 0x68, 0x49, 0x35, 0x46, 0x71, 0x43, 0x4b, 0x78, 0x64, 0x38, 0x37, 0x6c, 0x4a, 0x6a, 0x61, 0x31, 0x4e, 0x31, 0x4c, 0x38, 0x57, 0x36, 0x68, 0x46, 0x64, 0x7a, 0x32, 0x6b, 0x32, 0x66, 0x67, 0x32, 0x36, 0x72, 0x42, 0x48, 0x66, 0x71, 0x44, 0x38, 0x52, 0x54, 0x7a, 0xa7, 0x61, 0x39, 0x6d, 0x6d, 0x53, 0x36, 0x79, 0x77, 0x4b, 0x41, 0x49, 0x31, 0x56, 0x58, 0x76, 0x77, 0x61, 0x42, 0x70, 0x4d, 0x46, 0x62, 0x6f, 0x6e, 0x6e, 0x46, 0x6f, 0x76, 0x52, 0x34, 0x49, 0x74, 0x49, 0x46, 0x70, 0x64, 0x67, 0x4d, 0x65, 0x79, 0xa1, 0x61, 0x33, 0x68, 0x39, 0x64, 0x74, 0x74, 0x4c, 0x4e, 0x46, 0x51, 0x63, 0x78, 0x37, 0x4c, 0xa1, 0x61, 0x74, 0x69, 0x49, 0x30, 0x67, 0x7a, 0x56, 0x37, 0x6b, 0x77, 0x65, 0x63, 0x59, 0x78, 0x6f, 0x6a, 0x63, 0x70, 0x49, 0x4f, 0x4f, 0x35, 0x5a, 0x34, 0x58, 0x64, 0x62, 0x6d, 0x68, 0xa1, 0x61, 0x39, 0x64, 0x56, 0x61, 0x7a, 0x70, 0x65, 0x69, 0x43, 0x54, 0x54, 0x79, 0x69, 0x7a, 0x6e, 0x74, 0x32, 0x4a, 0x33, 0x54, 0x69, 0x75, 0x63, 0x50, 0x78, 0x70, 0x78, 0x43, 0x57, 0x38, 0x32, 0x31, 0x5f, 0x70, 0x63, 0x64, 0x54, 0x6c, 0x68, 0x59, 0x71, 0x39, 0x6a, 0x52, 0x39, 0x31, 0x79, 0x76, 0x78, 0x53, 0x43, 0x78, 0x46, 0x50, 0x4a, 0x48, 0x67, 0x33, 0x45, 0x44, 0x57, 0x7a, 0x58, 0x52, 0x61, 0x54, 0x38, 0x4d, 0x65, 0x71, 0x6d, 0x69, 0x4e, 0x53, 0x47, 0x78, 0x38, 0x75, 0x44, 0x58, 0x44, 0x53, 0x6c, 0x58, 0x42, 0x39, 0x65, 0x66, 0x58, 0x54, 0x4f, 0x32, 0x70, 0x34, 0x45, 0x69, 0x79, 0x39, 0x6b, 0x59, 0x6c, 0x51, 0x63, 0x76, 0x32, 0x78, 0x6c, 0x58, 0x6b, 0x71, 0x73, 0x6a, 0x42, 0x73, 0x48, 0x34, 0x73, 0x75, 0x68, 0x49, 0x4c, 0x6d, 0x5f, 0x58, 0x75, 0x59, 0x48, 0x6d, 0x37, 0x65, 0x6b, 0x58, 0x67, 0x39, 0x64, 0x69, 0x38, 0x30, 0x57, 0x57, 0x67, 0x41, 0x58, 0x32, 0x76, 0x78, 0x63, 0x61, 0x35, 0x38, 0x69, 0x52, 0x50, 0x69, 0x78, 0x30, 0x4b, 0x6b, 0x32, 0x4a, 0x45, 0x49, 0x77, 0x6e, 0x4e, 0x67, 0x6e, 0x30, 0x72, 0x42, 0x67, 0x35, 0x55, 0x44, 0x44, 0x79, 0x4c, 0x79, 0x59, 0x70, 0x6d, 0x6b, 0x56, 0x4f, 0x6e, 0x54, 0x4f, 0x32, 0x78, 0x56, 0x37, 0x43, 0x4d, 0x4c, 0x51, 0x6a, 0x6d, 0x55, 0x76, 0x52, 0x6b, 0x4b, 0x64, 0x62, 0x7a, 0x4c, 0x63, 0x61, 0x61, 0x33, 0x4f, 0x67, 0x38, 0x49, 0x67, 0x78, 0x1a, 0x6d, 0x68, 0x43, 0x38, 0x70, 0x73, 0x4e, 0x31, 0x39, 0x6b, 0x64, 0x57, 0x57, 0x5a, 0x6e, 0x77, 0x5a, 0x53, 0x61, 0x67, 0x7a, 0x79, 0x47, 0x41, 0x50, 0x4f, 0x78, 0x59, 0x61, 0x74, 0x65, 0x5f, 0x51, 0x50, 0x6d, 0x42, 0x4d, 0x33, 0x6c, 0x75, 0x67, 0x70, 0x33, 0x32, 0x72, 0x78, 0x75, 0x76, 0x4d, 0x43, 0x6d, 0x56, 0x77, 0x57, 0x37, 0x5a, 0x4c, 0x45, 0x6b, 0x4f, 0x71, 0x62, 0x55, 0x65, 0x51, 0x61, 0x56, 0x4a, 0x49, 0x5a, 0x69, 0x6e, 0x39, 0x69, 0x43, 0x59, 0x43, 0x77, 0x6c, 0x79, 0x4c, 0x79, 0x75, 0x6b, 0x6f, 0x65, 0x70, 0x6b, 0x78, 0x4b, 0x4a, 0x52, 0x48, 0x50, 0x4c, 0x42, 0x6b, 0x54, 0x6b, 0x48, 0x5f, 0x6d, 0x70, 0x76, 0x70, 0x37, 0x79, 0x53, 0x55, 0x4a, 0x79, 0x55, 0x6a, 0x41, 0x30, 0x63, 0x70, 0x6c, 0x4e, 0x50, 0x61, 0x43, 0x63, 0x31, 0x34, 0x35, 0x4a, 0x4c, 0x38, 0x36, 0x78, 0x58, 0x37, 0x49, 0x52, 0x41, 0x32, 0x41, 0x75, 0x5f, 0x5a, 0x39, 0x35, 0x33, 0x72, 0x71, 0x41, 0x39, 0x38, 0x65, 0x5a, 0x65, 0x4f, 0x39, 0x70, 0x55, 0x69, 0x51, 0x6a, 0x36, 0x7a, 0x4c, 0x37, 0x45, 0x49, 0x51, 0x6b, 0x79, 0x49, 0x52, 0x52, 0x59, 0x70, 0x36, 0x37, 0x57, 0x4f, 0x78, 0x52, 0x34, 0x54, 0x59, 0x4f, 0x47, 0x41, 0x63, 0x6a, 0x59, 0x6b, 0x5f, 0x5f, 0x67, 0x57, 0x63, 0x78, 0x41, 0x6a, 0x77, 0x36, 0x47, 0x34, 0x6b, 0x48, 0x50, 0x45, 0x39, 0x32, 0x42, 0x30, 0x6f, 0x39, 0x36, 0x76, 0x73, 0x54, 0x69, 0x72, 0x5a, 0x53, 0x4e, 0x6d, 0x49, 0x30, 0x47, 0x58, 0x74, 0x6a, 0x57, 0x42, 0x69, 0x6d, 0x62, 0x42, 0x53, 0x78, 0x2a, 0x39, 0x46, 0x37, 0x6a, 0x68, 0x67, 0x58, 0x72, 0x72, 0x49, 0x50, 0x50, 0x6d, 0x55, 0x42, 0x30, 0x6d, 0x4a, 0x36, 0x56, 0x68, 0x4f, 0x62, 0x58, 0x69, 0x4f, 0x54, 0x32, 0x61, 0x69, 0x45, 0x65, 0x54, 0x68, 0x41, 0x63, 0x6b, 0x42, 0x49, 0x44, 0x4c, 0x6c, 0x78, 0x2b, 0x4d, 0x32, 0x48, 0x43, 0x6c, 0x41, 0x70, 0x4d, 0x34, 0x36, 0x47, 0x57, 0x6a, 0x57, 0x6d, 0x48, 0x4d, 0x72, 0x71, 0x30, 0x33, 0x4d, 0x31, 0x4c, 0x4d, 0x42, 0x54, 0x4f, 0x69, 0x54, 0x4b, 0x46, 0x78, 0x4b, 0x31, 0x4a, 0x45, 0x52, 0x6e, 0x4c, 0x70, 0x5a, 0x43, 0x78, 0x46, 0x64, 0x4b, 0x37, 0x7a, 0x4c, 0x4e, 0x6c, 0x62, 0x4a, 0x42, 0x46, 0x30, 0x53, 0x39, 0x39, 0x32, 0x35, 0x31, 0x38, 0x66, 0x76, 0x4a, 0x67, 0x6a, 0x70, 0x77, 0x6e, 0x55, 0x31, 0x57, 0x33, 0x4b, 0x35, 0x55, 0x63, 0x43, 0x43, 0x57, 0x5f, 0x49, 0x5a, 0x6b, 0x46, 0x48, 0x42, 0x64, 0x5f, 0x38, 0x56, 0x76, 0x37, 0x49, 0x6b, 0x63, 0x4c, 0x70, 0x5f, 0x59, 0x36, 0x49, 0x6e, 0x34, 0x42, 0x34, 0x35, 0x4d, 0x45, 0x43, 0x70, 0x78, 0x78, 0x18, 0x6f, 0x4a, 0x5f, 0x46, 0x65, 0x4a, 0x38, 0x6e, 0x79, 0x36, 0x6b, 0x53, 0x73, 0x6c, 0x74, 0x64, 0x78, 0x58, 0x35, 0x78, 0x74, 0x74, 0x56, 0x6b, 0x78, 0x31, 0x36, 0x43, 0x4e, 0x31, 0x47, 0x70, 0x6d, 0x74, 0x7a, 0x6e, 0x6d, 0x33, 0x5f, 0x6f, 0x33, 0x6d, 0x6d, 0x52, 0x76, 0x62, 0x62, 0x70, 0x37, 0x36, 0x68, 0x67, 0x74, 0x74, 0x6e, 0x4f, 0x37, 0x4e, 0x68, 0x74, 0x6c, 0x6b, 0x6f, 0x39, 0x38, 0x6f, 0x48, 0x4b, 0x37, 0x69, 0x46, 0x42, 0x61, 0x79, 0x44, 0x78, 0x32, 0x77, 0x72, 0x53, 0x50, 0x74, 0x64, 0x4b, 0x31, 0x68, 0x70, 0x73, 0x75, 0x49, 0x74, 0x35, 0x52, 0x5f, 0x35, 0x47, 0x58, 0x78, 0x54, 0x68, 0x56, 0x30, 0x73, 0x70, 0x6d, 0x57, 0x72, 0x65, 0x4e, 0x66, 0x7a, 0x32, 0x61, 0x7a, 0x57, 0x30, 0x50, 0x46, 0x4d, 0x4b, 0x4d, 0x54, 0x43, 0x7a, 0x59, 0x46, 0x7a, 0x78, 0x45, 0x36, 0x72, 0x42, 0x4f, 0x4a, 0x5a, 0x77, 0x31, 0x74, 0x78, 0x32, 0x31, 0x77, 0x4b, 0x43, 0x62, 0x37, 0x59, 0x71, 0x78, 0x4f, 0x49, 0x73, 0x73, 0x66, 0x4c, 0x36, 0x48, 0x65, 0x66, 0x47, 0x4d, 0x76, 0x79, 0x72, 0x36, 0x53, 0x31, 0x65, 0x37, 0x78, 0x4a, 0x71, 0x43, 0x64, 0x59, 0x72, 0x31, 0x4f, 0x57, 0x48, 0x6c, 0x68, 0x78, 0x71, 0x36, 0x47, 0x33, 0x52, 0x54, 0x38, 0x53, 0x38, 0x63, 0x4a, 0x39, 0x52, 0x39, 0x4c, 0x78, 0x18, 0x69, 0x36, 0x56, 0x55, 0x51, 0x6b, 0x44, 0x38, 0x6a, 0x50, 0x5a, 0x41, 0x50, 0x48, 0x6a, 0x4b, 0x49, 0x34, 0x5a, 0x4a, 0x71, 0x4f, 0x43, 0x74, 0x78, 0x3e, 0x76, 0x77, 0x55, 0x30, 0x54, 0x4d, 0x6d, 0x6b, 0x6f, 0x78, 0x58, 0x53, 0x49, 0x4a, 0x6a, 0x65, 0x62, 0x50, 0x6e, 0x5a, 0x72, 0x68, 0x56, 0x4b, 0x54, 0x53, 0x6d, 0x67, 0x48, 0x79, 0x51, 0x6d, 0x48, 0x51, 0x31, 0x54, 0x44, 0x69, 0x63, 0x5a, 0x6f, 0x4b, 0x63, 0x63, 0x32, 0x68, 0x69, 0x54, 0x54, 0x64, 0x6e, 0x67, 0x78, 0x53, 0x47, 0x37, 0x71, 0x41, 0x76, 0x35, 0x6c, 0x67, 0x63, 0x72, 0x79, 0x6a, 0x78, 0x50, 0x7a, 0x52, 0x6f, 0x71, 0x79, 0x6f, 0x6d, 0x47, 0x46, 0x69, 0x77, 0x4e, 0x67, 0x50, 0x32, 0x65, 0x4a, 0x50, 0x30, 0x75, 0x79, 0x67, 0x51, 0x47, 0x52, 0x36, 0x52, 0x68, 0x45, 0x46, 0x47, 0x55, 0x48, 0x33, 0x4c, 0x62, 0x47, 0x66, 0x59, 0x4a, 0x4d, 0x6d, 0x57, 0x4d, 0x73, 0x30, 0x4e, 0x74, 0x61, 0x63, 0x45, 0x35, 0x7a, 0x6c, 0x59, 0x4f, 0x61, 0x69, 0x47, 0x52, 0x51, 0x56, 0x51, 0x67, 0x4b, 0x69, 0x56, 0x42, 0x6b, 0x4c, 0x69, 0x43, 0x56, 0x65, 0x38, 0x47, 0x44, 0x58, 0x59, 0x30, 0x6e, 0x6f, 0x6a, 0x4f, 0x65, 0x38, 0x34, 0x57, 0x50, 0x30, 0x45, 0x64, 0x31, 0x4b, 0x34, 0x78, 0x1b, 0x77, 0x70, 0x63, 0x69, 0x78, 0x36, 0x43, 0x46, 0x44, 0x58, 0x78, 0x49, 0x53, 0x4e, 0x4e, 0x65, 0x4f, 0x30, 0x4d, 0x5f, 0x67, 0x36, 0x39, 0x4f, 0x57, 0x47, 0x33, 0x78, 0x19, 0x72, 0x74, 0x76, 0x52, 0x30, 0x66, 0x34, 0x39, 0x71, 0x63, 0x61, 0x4b, 0x4e, 0x5f, 0x54, 0x4b, 0x6a, 0x4a, 0x39, 0x39, 0x66, 0x57, 0x42, 0x38, 0x4a, 0x78, 0x31, 0x74, 0x67, 0x32, 0x35, 0x77, 0x4e, 0x65, 0x6c, 0x65, 0x71, 0x4c, 0x4c, 0x46, 0x4f, 0x6c, 0x5f, 0x53, 0x4d, 0x65, 0x73, 0x6a, 0x6a, 0x33, 0x51, 0x30, 0x6d, 0x71, 0x68, 0x6e, 0x4b, 0x51, 0x37, 0x6b, 0x76, 0x54, 0x6c, 0x7a, 0x44, 0x72, 0x71, 0x68, 0x79, 0x57, 0x46, 0x44, 0x5a, 0x38, 0x6f, 0x30, 0x6d, 0x68, 0x6f, 0x62, 0x46, 0x6a, 0x6e, 0x35, 0x4b, 0x67, 0x46, 0x77, 0x4d, 0x44, 0xa4, 0x61, 0x49, 0x6a, 0x65, 0x34, 0x4b, 0x46, 0x55, 0x76, 0x46, 0x56, 0x73, 0x63, 0x64, 0x30, 0x5f, 0x59, 0x4a, 0x6d, 0x44, 0x52, 0x57, 0x42, 0x75, 0x66, 0x43, 0x37, 0x4a, 0x56, 0x47, 0x4f, 0x57, 0x63, 0x65, 0x71, 0x75, 0x6b, 0x5a, 0x37, 0x56, 0x67, 0x6f, 0x4f, 0x4a, 0x74, 0x52, 0x45, 0x53, 0x61, 0x43, 0x6a, 0x4e, 0x32, 0x77, 0x6e, 0x38, 0x4f, 0x49, 0x57, 0x67, 0x51, 0x61, 0x49, 0x78, 0x33, 0x71, 0x71, 0x70, 0x6a, 0x5a, 0x6c, 0x4d, 0x38, 0x4c, 0x45, 0x69, 0x61, 0x55, 0x4d, 0x4c, 0x4b, 0x36, 0x43, 0x63, 0x59, 0x37, 0x4f, 0x7a, 0x35, 0x51, 0x64, 0x55, 0x4a, 0x39, 0x6a, 0x4d, 0x53, 0x35, 0x35, 0x50, 0x44, 0x72, 0x43, 0x49, 0x64, 0x4a, 0x75, 0x43, 0x71, 0x51, 0x6f, 0x5a, 0x55, 0x4b, 0x45, 0x6a, 0x64, 0x5a, 0x6f, 0x36, 0x50, 0x78, 0x26, 0x30, 0x64, 0x70, 0x4d, 0x7a, 0x42, 0x64, 0x58, 0x68, 0x50, 0x50, 0x34, 0x52, 0x53, 0x65, 0x44, 0x66, 0x6e, 0x48, 0x75, 0x55, 0x59, 0x73, 0x4f, 0x33, 0x76, 0x4c, 0x5f, 0x76, 0x54, 0x64, 0x69, 0x54, 0x6e, 0x69, 0x56, 0x52, 0x55, 0x70, 0x46, 0x50, 0x62, 0x69, 0x6c, 0x5a, 0x31, 0x50, 0x59, 0x70, 0x4d, 0x7a, 0x62, 0x30, 0x45, 0x50, 0x78, 0x45, 0x6e, 0x55, 0x32, 0x6a, 0x4b, 0x35, 0x62, 0x32, 0x67, 0x65, 0x43, 0x6e, 0x54, 0x6e, 0x56, 0x56, 0x63, 0x42, 0x49, 0x34, 0x44, 0x64, 0x62, 0x64, 0x77, 0x74, 0x59, 0x41, 0x52, 0x77, 0x52, 0x38, 0x35, 0x74, 0x67, 0x52, 0x4f, 0x79, 0x67, 0x47, 0x30, 0x79, 0x38, 0x35, 0x4e, 0x6f, 0x30, 0x77, 0x53, 0x4b, 0x30, 0x52, 0x49, 0x71, 0x41, 0x35, 0x6d, 0x6a, 0x4a, 0x6b, 0x47, 0x73, 0x51, 0x6e, 0x42, 0x4f, 0x62, 0x4f, 0x4a, 0x78, 0x1c, 0x68, 0x61, 0x67, 0x39, 0x78, 0x63, 0x4e, 0x62, 0x5a, 0x4e, 0x4e, 0x38, 0x57, 0x6d, 0x37, 0x6e, 0x4e, 0x30, 0x71, 0x48, 0x7a, 0x50, 0x46, 0x4c, 0x47, 0x50, 0x7a, 0x78, 0xa4, 0x61, 0x41, 0x65, 0x49, 0x61, 0x47, 0x56, 0x70, 0x67, 0x38, 0x42, 0x6d, 0x36, 0x4b, 0x58, 0x68, 0xa1, 0x61, 0x33, 0x62, 0x41, 0x78, 0x65, 0x66, 0x54, 0x63, 0x4a, 0x55, 0x68, 0x39, 0x47, 0x48, 0x5a, 0x69, 0x6a, 0x34, 0x43, 0x66, 0x37, 0x72, 0x33, 0x50, 0x59, 0x63, 0xa1, 0x61, 0x68, 0x68, 0x54, 0x55, 0x42, 0x39, 0x74, 0x44, 0x5f, 0x6e, 0x78, 0x1e, 0x62, 0x56, 0x4c, 0x38, 0x51, 0x61, 0x34, 0x4c, 0x52, 0x70, 0x42, 0x41, 0x76, 0x54, 0x48, 0x6c, 0x5f, 0x63, 0x52, 0x4e, 0x72, 0x77, 0x68, 0x36, 0x58, 0x72, 0x52, 0x4f, 0x61, 0x47, 0x78, 0x27, 0x75, 0x36, 0x4a, 0x4f, 0x39, 0x6d, 0x56, 0x50, 0x35, 0x51, 0x63, 0x36, 0x32, 0x36, 0x55, 0x6e, 0x36, 0x55, 0x58, 0x70, 0x51, 0x39, 0x59, 0x36, 0x56, 0x47, 0x4e, 0x50, 0x64, 0x41, 0x58, 0x49, 0x4f, 0x43, 0x47, 0x6f, 0x39, 0x44, 0x73, 0x78, 0x26, 0x41, 0x77, 0x53, 0x54, 0x5a, 0x33, 0x4f, 0x52, 0x4a, 0x47, 0x79, 0x68, 0x43, 0x61, 0x63, 0x58, 0x43, 0x71, 0x79, 0x46, 0x31, 0x45, 0x57, 0x66, 0x34, 0x51, 0x6c, 0x73, 0x62, 0x50, 0x50, 0x53, 0x46, 0x66, 0x36, 0x4e, 0x49, 0x57, 0x78, 0x28, 0x41, 0x61, 0x47, 0x72, 0x32, 0x34, 0x50, 0x70, 0x4b, 0x59, 0x6a, 0x58, 0x75, 0x6c, 0x32, 0x77, 0x79, 0x54, 0x70, 0x4a, 0x62, 0x42, 0x75, 0x6e, 0x46, 0x74, 0x36, 0x68, 0x76, 0x79, 0x62, 0x6a, 0x32, 0x54, 0x6b, 0x43, 0x5a, 0x48, 0x4a, 0x4c, 0x73, 0x46, 0x30, 0x62, 0x35, 0x71, 0x4e, 0x69, 0x32, 0x74, 0x44, 0x44, 0x50, 0x49, 0x7a, 0x49, 0x49, 0x50, 0x51, 0x65, 0xa5, 0x61, 0x51, 0x66, 0x36, 0x44, 0x41, 0x78, 0x61, 0x33, 0x65, 0x76, 0x62, 0x7a, 0x4d, 0x72, 0x66, 0x5a, 0x4b, 0x59, 0x46, 0x59, 0x6f, 0x62, 0x6c, 0x32, 0x6b, 0x61, 0x64, 0x6a, 0x4a, 0x30, 0x4f, 0x4b, 0x34, 0x74, 0x4d, 0x68, 0x63, 0x38, 0x65, 0x36, 0x67, 0x6e, 0x61, 0x46, 0x48, 0x31, 0x64, 0x5a, 0x65, 0x65, 0x37, 0x6d, 0x62, 0x41, 0x68, 0x59, 0x4a, 0x6f, 0x43, 0x39, 0x34, 0x47, 0x46, 0x73, 0x61, 0x70, 0x57, 0x79, 0x76, 0x4b, 0x56, 0x45, 0x74, 0x79, 0x43, 0x62, 0x42, 0x61, 0x71, 0x44, 0x72, 0x71, 0x33, 0x78, 0x34, 0x4a, 0x6d, 0x6a, 0x43, 0x61, 0x61, 0x7a, 0x58, 0x67, 0x57, 0x54, 0x51, 0x64, 0x75, 0x51, 0x48, 0x52, 0x31, 0x4f, 0x67, 0x6f, 0x34, 0x61, 0x31, 0x59, 0x73, 0x69, 0x4d, 0x61, 0x30, 0x6a, 0x6c, 0x75, 0x4c, 0x32, 0x6a, 0x55, 0x48, 0x50, 0x35, 0x5a, 0x42, 0x42, 0x64, 0x58, 0x6c, 0x6c, 0x76, 0x69, 0x6a, 0x77, 0x50, 0x78, 0x37, 0x6b, 0x4c, 0x6e, 0x6e, 0x67, 0x70, 0x30, 0x38, 0x33, 0x5a, 0x6e, 0x6e, 0x43, 0x65, 0x62, 0x33, 0x35, 0x43, 0x65, 0x62, 0x44, 0x63, 0x77, 0x35, 0x30, 0x36, 0x68, 0x78, 0x72, 0x76, 0x70, 0x5a, 0x33, 0x58, 0x30, 0x64, 0x43, 0x57, 0x5f, 0x6f, 0x58, 0x6b, 0x46, 0x47, 0x50, 0x72, 0x6e, 0x46, 0x67, 0x6a, 0x39, 0x36, 0x72, 0x5f, 0x76, 0xa3, 0x61, 0x47, 0x74, 0x47, 0x6e, 0x44, 0x48, 0x32, 0x69, 0x79, 0x54, 0x70, 0x78, 0x47, 0x52, 0x31, 0x36, 0x42, 0x4a, 0x4e, 0x55, 0x57, 0x38, 0x66, 0x47, 0x57, 0x42, 0x35, 0x4d, 0x43, 0x6d, 0x69, 0x4d, 0x42, 0x42, 0x58, 0x46, 0x7a, 0x57, 0x65, 0x37, 0x63, 0x61, 0x6b, 0x64, 0x42, 0x32, 0x4a, 0x73, 0x6c, 0x5a, 0x63, 0x56, 0x67, 0x33, 0x4e, 0x71, 0x66, 0x6d, 0x47, 0x6b, 0x72, 0x66, 0x70, 0x47, 0x62, 0x30, 0x4e, 0x4d, 0xa3, 0x61, 0x47, 0x68, 0x4e, 0x30, 0x32, 0x41, 0x33, 0x58, 0x66, 0x68, 0x61, 0x63, 0x6d, 0x48, 0x58, 0x74, 0x58, 0x4a, 0x74, 0x65, 0x41, 0x6c, 0x49, 0x71, 0x71, 0x64, 0x61, 0x4a, 0xa2, 0x61, 0x35, 0x60, 0x62, 0x77, 0x6f, 0xa1, 0x61, 0x58, 0x62, 0x54, 0x72, 0x78, 0x1e, 0x47, 0x59, 0x58, 0x5a, 0x6d, 0x46, 0x79, 0x39, 0x6c, 0x72, 0x65, 0x4e, 0x6b, 0x76, 0x6c, 0x37, 0x4b, 0x44, 0x6c, 0x4e, 0x71, 0x49, 0x6c, 0x4a, 0x54, 0x62, 0x38, 0x6f, 0x39, 0x50, 0x78, 0x51, 0x65, 0x4b, 0x36, 0x37, 0x4b, 0x43, 0x4d, 0x66, 0x35, 0x76, 0x4e, 0x62, 0x56, 0x4a, 0x53, 0x6b, 0x69, 0x5a, 0x36, 0x79, 0x49, 0x6a, 0x66, 0x53, 0x36, 0x54, 0x31, 0x42, 0x53, 0x37, 0x54, 0x65, 0x35, 0x69, 0x44, 0x42, 0x35, 0x76, 0x54, 0x46, 0x6a, 0x4e, 0x72, 0x39, 0x79, 0x34, 0x33, 0x74, 0x57, 0x71, 0x70, 0x56, 0x53, 0x75, 0x55, 0x35, 0x55, 0x4d, 0x68, 0x4c, 0x51, 0x78, 0x6f, 0x34, 0x5f, 0x55, 0x63, 0x69, 0x70, 0x67, 0x4b, 0x38, 0x75, 0x78, 0x6d, 0x7a, 0x6a, 0x49, 0x63, 0x57, 0x67, 0x77, 0x31, 0x31, 0x34, 0x38, 0x79, 0x79, 0x32, 0x6b, 0x7a, 0x42, 0x38, 0x70, 0x67, 0x4a, 0x64, 0x57, 0x57, 0x45, 0x43, 0x6b, 0x75, 0x66, 0x4a, 0x78, 0x24, 0x58, 0x71, 0x5a, 0x34, 0x49, 0x51, 0x59, 0x49, 0x6f, 0x45, 0x4e, 0x31, 0x51, 0x53, 0x61, 0x78, 0x59, 0x33, 0x70, 0x65, 0x34, 0x37, 0x55, 0x52, 0x33, 0x4a, 0x50, 0x35, 0x53, 0x34, 0x6a, 0x69, 0x5f, 0x77, 0x5a, 0x49, 0x68, 0x6e, 0x65, 0x37, 0x38, 0x62, 0x38, 0x6b, 0x69, 0x78, 0x52, 0x51, 0x50, 0x79, 0x5a, 0x55, 0x68, 0x7a, 0x46, 0x76, 0x50, 0x33, 0x6f, 0x63, 0x38, 0x74, 0x64, 0x6e, 0x42, 0x72, 0x36, 0x61, 0x79, 0x70, 0x6b, 0x4b, 0x6a, 0x65, 0x67, 0x66, 0x70, 0x71, 0x78, 0x62, 0x55, 0x39, 0x59, 0x61, 0x6d, 0x4e, 0x57, 0x74, 0x66, 0x33, 0x72, 0x48, 0x75, 0x57, 0x30, 0x70, 0x43, 0x57, 0x70, 0x50, 0x71, 0x34, 0x34, 0x39, 0x5f, 0x62, 0x66, 0x34, 0x43, 0x6d, 0x61, 0x5f, 0x48, 0x47, 0x66, 0x6c, 0x76, 0x77, 0x37, 0x50, 0x5a, 0x63, 0x4d, 0x79, 0x45, 0x52, 0x61, 0x52, 0x48, 0x78, 0x2a, 0x71, 0x69, 0x4b, 0x32, 0x42, 0x70, 0x53, 0x51, 0x4a, 0x4d, 0x61, 0x68, 0x35, 0x6b, 0x39, 0x68, 0x6a, 0x48, 0x57, 0x46, 0x63, 0x78, 0x36, 0x54, 0x5f, 0x38, 0x4b, 0x45, 0x41, 0x34, 0x56, 0x79, 0x77, 0x66, 0x50, 0x57, 0x50, 0x47, 0x63, 0x46, 0x74, 0x58, 0x78, 0x3e, 0x72, 0x4f, 0x70, 0x59, 0x43, 0x5f, 0x42, 0x66, 0x6f, 0x6a, 0x51, 0x6e, 0x48, 0x70, 0x65, 0x69, 0x45, 0x36, 0x4b, 0x6d, 0x65, 0x38, 0x4b, 0x71, 0x33, 0x61, 0x7a, 0x48, 0x69, 0x43, 0x4a, 0x45, 0x4b, 0x76, 0x6a, 0x32, 0x58, 0x62, 0x51, 0x32, 0x73, 0x6f, 0x45, 0x57, 0x62, 0x78, 0x4e, 0x55, 0x52, 0x47, 0x34, 0x76, 0x43, 0x52, 0x6d, 0x57, 0x38, 0x58, 0x59, 0x35, 0x78, 0x49, 0x78, 0x1f, 0x4b, 0x67, 0x67, 0x77, 0x38, 0x6e, 0x47, 0x52, 0x44, 0x77, 0x36, 0x76, 0x49, 0x53, 0x53, 0x50, 0x62, 0x4f, 0x4b, 0x39, 0x43, 0x6d, 0x45, 0x42, 0x30, 0x6c, 0x35, 0x6b, 0x33, 0x49, 0x68, 0x78, 0x2d, 0x6e, 0x4a, 0x6e, 0x52, 0x57, 0x70, 0x75, 0x51, 0x5f, 0x61, 0x6d, 0x4a, 0x43, 0x6b, 0x4b, 0x36, 0x6d, 0x6a, 0x54, 0x5a, 0x58, 0x35, 0x4b, 0x66, 0x4d, 0x36, 0x4f, 0x67, 0x39, 0x53, 0x4a, 0x6b, 0x59, 0x45, 0x4b, 0x66, 0x6e, 0x4f, 0x61, 0x5f, 0x44, 0x37, 0x69, 0x39, 0x59, 0x66, 0x34, 0x75, 0x74, 0x33, 0x33, 0x46, 0xa3, 0x61, 0x55, 0x64, 0x70, 0x74, 0x5a, 0x65, 0x63, 0x51, 0x5a, 0x57, 0x64, 0x79, 0x49, 0x39, 0x50, 0x62, 0x56, 0x62, 0x6c, 0x50, 0x32, 0x45, 0x48, 0x67, 0x68, 0x68, 0x44, 0x43, 0x5a, 0x56, 0x57, 0x6e, 0x59, 0x5a, 0x54, 0x4a, 0x38, 0x43, 0x43, 0x43, 0x6b, 0x35, 0x6b, 0x51, 0x57, 0x65, 0x78, 0x1c, 0x70, 0x68, 0x49, 0x4f, 0x6f, 0x48, 0x48, 0x34, 0x72, 0x55, 0x6e, 0x72, 0x62, 0x55, 0x36, 0x76, 0x38, 0x43, 0x33, 0x42, 0x4e, 0x4d, 0x42, 0x73, 0x45, 0x72, 0x70, 0x5a, 0x78, 0x1d, 0x72, 0x6b, 0x55, 0x7a, 0x63, 0x32, 0x46, 0x6d, 0x6b, 0x5f, 0x75, 0x74, 0x52, 0x69, 0x47, 0x78, 0x51, 0x6a, 0x72, 0x43, 0x48, 0x36, 0x30, 0x48, 0x79, 0x6d, 0x7a, 0x6e, 0x42, 0x78, 0x46, 0x6c, 0x6c, 0x41, 0x48, 0x50, 0x38, 0x66, 0x62, 0x38, 0x44, 0x79, 0x4c, 0x59, 0x68, 0x48, 0x51, 0x6e, 0x56, 0x52, 0x4f, 0x72, 0x6c, 0x67, 0x4c, 0x32, 0x65, 0x6c, 0x44, 0x43, 0x56, 0x43, 0x64, 0x50, 0x4a, 0x4f, 0x71, 0x70, 0x44, 0x45, 0x50, 0x50, 0x75, 0x32, 0x5a, 0x32, 0x79, 0x56, 0x4a, 0x47, 0x32, 0x7a, 0x50, 0x72, 0x69, 0x39, 0x6f, 0x6e, 0x4b, 0x77, 0x78, 0x45, 0x43, 0x69, 0x48, 0x79, 0x4d, 0x7a, 0x6f, 0x7a, 0x41, 0x71, 0x70, 0x31, 0x34, 0x72, 0x6e, 0x71, 0x6b, 0x58, 0x74, 0x76, 0x71, 0x38, 0x4a, 0x38, 0x38, 0x79, 0x45, 0x77, 0x64, 0x6c, 0x76, 0x52, 0x75, 0x74, 0x6f, 0x75, 0x75, 0x37, 0x6b, 0x63, 0x31, 0x6c, 0x45, 0x63, 0x39, 0x67, 0x77, 0x62, 0x45, 0x74, 0x67, 0x73, 0x7a, 0x33, 0x67, 0x5f, 0x79, 0x49, 0x71, 0x69, 0x39, 0x4d, 0x5f, 0x54, 0x77, 0x4b, 0x5a, 0x75, 0x41, 0x4a, 0x63, 0xa2, 0x61, 0x75, 0x63, 0x4d, 0x62, 0x58, 0x62, 0x78, 0x6e, 0x6d, 0x5a, 0x74, 0x66, 0x4b, 0x4c, 0x4a, 0x65, 0x43, 0x36, 0x43, 0x48, 0x32, 0x6d, 0x6d, 0x4e, 0x47, 0x31, 0x72, 0x4d, 0x69, 0x5a, 0x33, 0x75, 0x70, 0x64, 0x58, 0x38, 0x78, 0x5c, 0x41, 0x76, 0x78, 0x64, 0x64, 0x44, 0x42, 0x57, 0x7a, 0x55, 0x72, 0x4a, 0x73, 0x63, 0x66, 0x4b, 0x30, 0x6d, 0x4a, 0x41, 0x32, 0x35, 0x58, 0x75, 0x30, 0x78, 0x62, 0x46, 0x72, 0x63, 0x4c, 0x64, 0x49, 0x72, 0x6a, 0x5f, 0x79, 0x36, 0x35, 0x53, 0x55, 0x45, 0x6a, 0x66, 0x43, 0x65, 0x72, 0x33, 0x43, 0x42, 0x74, 0x71, 0x49, 0x56, 0x7a, 0x6b, 0x4c, 0x33, 0x37, 0x7a, 0x6a, 0x6d, 0x53, 0x57, 0x65, 0x46, 0x42, 0x74, 0x50, 0x62, 0x67, 0x44, 0x74, 0x73, 0x4a, 0x74, 0x50, 0x55, 0x77, 0x62, 0x4d, 0x4f, 0x77, 0x30, 0x61, 0x75, 0x4e, 0x59, 0x61, 0x44, 0x30, 0x38, 0x78, 0x22, 0x6f, 0x43, 0x54, 0x63, 0x70, 0x78, 0x65, 0x68, 0x6e, 0x38, 0x7a, 0x6e, 0x77, 0x47, 0x37, 0x44, 0x6c, 0x7a, 0x74, 0x6d, 0x75, 0x54, 0x55, 0x31, 0x30, 0x37, 0x6f, 0x58, 0x68, 0x79, 0x47, 0x51, 0x41, 0x43, 0x78, 0x36, 0x72, 0x78, 0x48, 0x51, 0x39, 0x52, 0x34, 0x6e, 0x45, 0x36, 0x50, 0x71, 0x6e, 0x56, 0x68, 0x54, 0x4f, 0x55, 0x6b, 0x6d, 0x62, 0x4a, 0x41, 0x62, 0x74, 0x68, 0x46, 0x4e, 0x45, 0x76, 0x64, 0x37, 0x5f, 0x37, 0x56, 0x68, 0x4b, 0x68, 0x5f, 0x62, 0x50, 0x46, 0x41, 0x6e, 0x66, 0x48, 0x65, 0x4e, 0x5a, 0x59, 0x64, 0x33, 0x50, 0x66, 0x78, 0x22, 0x5f, 0x4c, 0x73, 0x54, 0x49, 0x43, 0x6c, 0x56, 0x51, 0x52, 0x30, 0x6b, 0x48, 0x79, 0x48, 0x59, 0x30, 0x5f, 0x66, 0x59, 0x59, 0x77, 0x61, 0x38, 0x46, 0x75, 0x32, 0x49, 0x64, 0x4d, 0x64, 0x50, 0x64, 0x31, 0x78, 0x42, 0x58, 0x4a, 0x73, 0x6c, 0x37, 0x47, 0x71, 0x77, 0x76, 0x76, 0x4f, 0x6a, 0x37, 0x64, 0x47, 0x65, 0x4b, 0x4d, 0x46, 0x51, 0x56, 0x7a, 0x48, 0x51, 0x7a, 0x5f, 0x72, 0x52, 0x55, 0x39, 0x33, 0x73, 0x7a, 0x63, 0x4f, 0x59, 0x77, 0x70, 0x31, 0x47, 0x48, 0x6d, 0x37, 0x46, 0x39, 0x51, 0x66, 0x6a, 0x50, 0x59, 0x39, 0x78, 0x77, 0x4d, 0x54, 0x56, 0x6b, 0x73, 0x37, 0x5f, 0x77, 0x45, 0x51, 0x73, 0x30, 0x77, 0x64, 0x77, 0x38, 0x69, 0x61, 0xa3, 0x61, 0x72, 0x62, 0x5a, 0x38, 0x62, 0x6d, 0x52, 0x67, 0x51, 0x72, 0x35, 0x59, 0x33, 0x4b, 0x43, 0x68, 0x46, 0x58, 0x77, 0x41, 0x68, 0x74, 0x33, 0x65, 0x68, 0x6c, 0x69, 0x6d, 0x54, 0x71, 0x52, 0x46, 0x43, 0x78, 0x31, 0x6e, 0x64, 0x66, 0x45, 0x5a, 0x58, 0x52, 0x49, 0x4e, 0x6d, 0x5a, 0x62, 0x48, 0x44, 0x6a, 0x6b, 0x56, 0x32, 0x71, 0x73, 0x4b, 0x38, 0x6f, 0x58, 0x6e, 0x41, 0x5a, 0x38, 0x39, 0x63, 0x52, 0x63, 0x56, 0x73, 0x6c, 0x4b, 0x73, 0x38, 0x4c, 0x44, 0x42, 0x42, 0x4c, 0x50, 0x47, 0x79, 0x33, 0x76, 0x4e, 0x78, 0x41, 0x52, 0x7a, 0x57, 0x52, 0x44, 0x47, 0x6f, 0x7a, 0x42, 0x67, 0x46, 0x44, 0x78, 0x34, 0x41, 0x4c, 0x77, 0x59, 0x54, 0x34, 0x72, 0x46, 0x75, 0x4f, 0x62, 0x70, 0x4e, 0x55, 0x6a, 0x76, 0x50, 0x49, 0x74, 0x6a, 0x54, 0x55, 0x32, 0x6b, 0x47, 0x35, 0x6b, 0x37, 0x49, 0x74, 0x78, 0x78, 0x32, 0x68, 0x49, 0x59, 0x52, 0x56, 0x59, 0x35, 0x6c, 0x38, 0x65, 0x76, 0x30, 0x33, 0x4e, 0x64, 0x6d, 0x30, 0x78, 0x78, 0x18, 0x74, 0x56, 0x61, 0x52, 0x43, 0x32, 0x73, 0x6b, 0x67, 0x6f, 0x39, 0x65, 0x45, 0x54, 0x31, 0x56, 0x7a, 0x52, 0x47, 0x6d, 0x57, 0x75, 0x4b, 0x50, 0x78, 0x28, 0x37, 0x37, 0x37, 0x77, 0x55, 0x54, 0x30, 0x7a, 0x36, 0x62, 0x56, 0x73, 0x33, 0x6a, 0x68, 0x39, 0x66, 0x5a, 0x51, 0x36, 0x65, 0x63, 0x79, 0x45, 0x5a, 0x78, 0x66, 0x6f, 0x65, 0x74, 0x54, 0x67, 0x5a, 0x65, 0x51, 0x51, 0x5a, 0x48, 0x69, 0x4e, 0x6c, 0x64, 0x62, 0x6f, 0x43, 0x73, 0x79, 0x66, 0x55, 0x61, 0x4d, 0x36, 0x4b, 0x78, 0x43, 0x62, 0x42, 0x70, 0x6e, 0x48, 0x59, 0x4a, 0x34, 0x44, 0x30, 0x63, 0x73, 0x77, 0x4e, 0x78, 0x31, 0x77, 0x72, 0x46, 0x35, 0x5f, 0x73, 0x4a, 0x54, 0x37, 0x6b, 0x6e, 0x35, 0x44, 0x6f, 0x4e, 0x6a, 0x73, 0x6d, 0x33, 0x66, 0x32, 0x49, 0x50, 0x32, 0x64, 0x6c, 0x54, 0x44, 0x75, 0x78, 0x43, 0x50, 0x44, 0x4c, 0x53, 0x6d, 0x69, 0x48, 0x5f, 0x6f, 0x46, 0x58, 0x71, 0x6b, 0x39, 0x64, 0x69, 0x53, 0x6a, 0x44, 0x4e, 0x68, 0x6e, 0x32, 0x31, 0x33, 0x66, 0x68, 0x32, 0x67, 0x78, 0x5b, 0x57, 0x52, 0x78, 0x73, 0x6f, 0x6b, 0x6c, 0x72, 0x4e, 0x30, 0x34, 0x5f, 0x4b, 0x56, 0x48, 0x34, 0x48, 0x68, 0x36, 0x57, 0x31, 0x51, 0x68, 0x44, 0x4c, 0x76, 0x4c, 0x36, 0x35, 0x6c, 0x33, 0x78, 0x64, 0x68, 0x78, 0x63, 0x44, 0x4f, 0x39, 0x44, 0x38, 0x71, 0x32, 0x49, 0x77, 0x41, 0x58, 0x35, 0x45, 0x4f, 0x4a, 0x42, 0x62, 0x7a, 0x39, 0x75, 0x34, 0x5a, 0x46, 0x4c, 0x43, 0x33, 0x69, 0x62, 0x78, 0x37, 0x5f, 0x46, 0x4c, 0x58, 0x64, 0x4a, 0x6f, 0x67, 0x37, 0x35, 0x41, 0x38, 0x49, 0x37, 0x48, 0x52, 0x4b, 0x47, 0x79, 0x46, 0x72, 0x55, 0x55, 0x31, 0x52, 0x6f, 0x71, 0x39, 0x6e, 0x6e, 0x33, 0x54, 0x31, 0x48, 0x46, 0x38, 0x6c, 0x69, 0x64, 0x63, 0x4d, 0x78, 0x2b, 0x38, 0x75, 0x5f, 0x62, 0x36, 0x71, 0x73, 0x51, 0x64, 0x70, 0x32, 0x36, 0x5a, 0x4f, 0x6a, 0x44, 0x6b, 0x79, 0x38, 0x61, 0x72, 0x36, 0x58, 0x46, 0x71, 0x34, 0x5a, 0x46, 0x5a, 0x78, 0x79, 0x59, 0x78, 0x6b, 0x37, 0x45, 0x63, 0x52, 0x49, 0x54, 0x41, 0x45, 0x48, 0x78, 0x18, 0x62, 0x6c, 0x6e, 0x4a, 0x77, 0x6e, 0x74, 0x4d, 0x50, 0x47, 0x6e, 0x57, 0x4d, 0x64, 0x68, 0x77, 0x4c, 0x62, 0x63, 0x4f, 0x47, 0x5a, 0x59, 0x52, 0xa6, 0x61, 0x6f, 0x62, 0x75, 0x33, 0x64, 0x45, 0x62, 0x6c, 0x38, 0x67, 0x48, 0x36, 0x5a, 0x31, 0x35, 0x52, 0x68, 0x62, 0x5f, 0x61, 0x64, 0x4c, 0x39, 0x50, 0x36, 0x68, 0x44, 0x42, 0x52, 0x77, 0x74, 0x75, 0x39, 0x76, 0x67, 0x70, 0x51, 0x4b, 0x30, 0x74, 0x51, 0x58, 0x64, 0x66, 0x4e, 0x46, 0x71, 0x6b, 0x76, 0x7a, 0x66, 0x64, 0x39, 0x77, 0x74, 0x37, 0x34, 0x78, 0x48, 0x65, 0x32, 0x31, 0x38, 0x59, 0x4e, 0x67, 0x63, 0x59, 0x69, 0x4b, 0x53, 0x76, 0x36, 0x6b, 0x41, 0x52, 0x35, 0x72, 0x5f, 0x32, 0x57, 0x43, 0x48, 0x6e, 0x65, 0x73, 0x6f, 0x4f, 0x32, 0x4c, 0x34, 0x6b, 0x72, 0x41, 0x31, 0x50, 0x4f, 0x63, 0x4b, 0x59, 0x37, 0x4c, 0x54, 0x4c, 0x30, 0x78, 0x22, 0x50, 0x57, 0x42, 0x78, 0x55, 0x56, 0x55, 0x45, 0x51, 0x5f, 0x7a, 0x47, 0x5a, 0x39, 0x56, 0x63, 0x46, 0x64, 0x49, 0x42, 0x55, 0x44, 0x42, 0x50, 0x79, 0x58, 0x66, 0x51, 0x78, 0x48, 0x59, 0x4a, 0x68, 0x35, 0x78, 0x53, 0x52, 0x43, 0x57, 0x37, 0x56, 0x79, 0x38, 0x48, 0x59, 0x30, 0x6a, 0x48, 0x48, 0x61, 0x57, 0x50, 0x6e, 0x39, 0x42, 0x4d, 0x72, 0x7a, 0x51, 0x76, 0x6c, 0x31, 0x66, 0x69, 0x63, 0x6e, 0x71, 0x56, 0x44, 0x61, 0x65, 0x44, 0x4c, 0x76, 0x31, 0x7a, 0x77, 0x63, 0x70, 0x6f, 0x39, 0x66, 0x34, 0x62, 0x71, 0x64, 0x39, 0x71, 0x73, 0x68, 0x46, 0x32, 0x71, 0x42, 0x55, 0x68, 0x6c, 0x76, 0x62, 0x32, 0x46, 0x39, 0x6d, 0x4b, 0x34, 0x7a, 0x61, 0x59, 0x70, 0x5f, 0x65, 0x52, 0x53, 0x44, 0x56, 0x46, 0x31, 0x75, 0x4a, 0x64, 0x32, 0x46, 0x48, 0x6c, 0xa4, 0x61, 0x33, 0x60, 0x63, 0x6f, 0x45, 0x47, 0x64, 0x53, 0x59, 0x46, 0x64, 0x64, 0x55, 0x48, 0x30, 0x68, 0x63, 0x61, 0x43, 0x68, 0x64, 0x69, 0x6f, 0x73, 0x75, 0xa1, 0x61, 0x36, 0x62, 0x6d, 0x57, 0x63, 0x32, 0x76, 0x30, 0x78, 0x35, 0x49, 0x5a, 0x38, 0x73, 0x68, 0x42, 0x66, 0x52, 0x6c, 0x51, 0x6b, 0x4a, 0x55, 0x45, 0x4e, 0x4c, 0x78, 0x41, 0x37, 0x34, 0x31, 0x36, 0x7a, 0x61, 0x35, 0x6f, 0x69, 0x68, 0x56, 0x61, 0x6b, 0x4b, 0x63, 0x56, 0x6b, 0x7a, 0x52, 0x42, 0x71, 0x53, 0x34, 0x55, 0x59, 0x79, 0x64, 0x38, 0x30, 0x4c, 0x55, 0x4a, 0x33, 0x6c, 0x45, 0x78, 0x2e, 0x59, 0x62, 0x61, 0x7a, 0x6f, 0x79, 0x64, 0x72, 0x43, 0x64, 0x6b, 0x45, 0x32, 0x67, 0x55, 0x6a, 0x33, 0x75, 0x5a, 0x67, 0x76, 0x43, 0x45, 0x6c, 0x51, 0x6f, 0x52, 0x77, 0x44, 0x64, 0x4c, 0x32, 0x59, 0x4c, 0x4d, 0x66, 0x78, 0x69, 0x6b, 0x74, 0x35, 0x70, 0x6b, 0x41, 0x38, 0x54, 0x78, 0x38, 0x7a, 0x73, 0x71, 0x43, 0x70, 0x39, 0x7a, 0x50, 0x6c, 0x54, 0x42, 0x49, 0x4a, 0x53, 0x71, 0x68, 0x73, 0x4e, 0x55, 0x76, 0x54, 0x5f, 0x65, 0x74, 0x54, 0x62, 0x53, 0x4b, 0x52, 0x7a, 0x57, 0x5a, 0x63, 0x4c, 0x77, 0x53, 0x31, 0x56, 0x73, 0x45, 0x77, 0x4e, 0x4f, 0x6b, 0x72, 0x78, 0x72, 0x43, 0x6a, 0x77, 0x53, 0x75, 0x78, 0x50, 0x36, 0x39, 0x74, 0x62, 0x4d, 0x44, 0x42, 0x5f, 0x4d, 0x6e, 0x78, 0x5a, 0x46, 0x77, 0x68, 0x57, 0x4a, 0x46, 0x52, 0x45, 0x77, 0x49, 0x52, 0xa2, 0x61, 0x32, 0x69, 0x55, 0x63, 0x59, 0x77, 0x66, 0x75, 0x49, 0x6a, 0x78, 0x64, 0x38, 0x4a, 0x46, 0x67, 0x70, 0x35, 0x78, 0x42, 0x69, 0x55, 0x67, 0x6f, 0x4b, 0x5a, 0x70, 0x39, 0x79, 0x36, 0x52, 0x32, 0x45, 0x61, 0x47, 0x78, 0x38, 0x6a, 0x36, 0x59, 0x50, 0x66, 0x4f, 0x59, 0x57, 0x4a, 0x67, 0x71, 0x50, 0x56, 0x79, 0x70, 0x48, 0x74, 0x33, 0x4d, 0x67, 0x38, 0x68, 0x74, 0x6b, 0x74, 0x4f, 0x39, 0x63, 0x73, 0x36, 0x47, 0x48, 0x4d, 0x30, 0x64, 0x57, 0x6e, 0x4a, 0x4b, 0x61, 0x51, 0x51, 0x4c, 0x65, 0x44, 0x78, 0x46, 0x63, 0x6c, 0x39, 0x42, 0x53, 0x6e, 0x59, 0x36, 0x30, 0x71, 0x58, 0x4b, 0x74, 0x67, 0x4f, 0x35, 0x32, 0x52, 0x38, 0x77, 0x41, 0x65, 0x5a, 0x4d, 0x62, 0x39, 0x5f, 0xa6, 0x61, 0x46, 0x66, 0x43, 0x45, 0x68, 0x45, 0x50, 0x5f, 0x62, 0x6b, 0x70, 0x68, 0x6c, 0x77, 0x78, 0x66, 0x4a, 0x6d, 0x42, 0x4c, 0x62, 0x6a, 0x77, 0xa1, 0x61, 0x55, 0x67, 0x4d, 0x36, 0x31, 0x71, 0x44, 0x61, 0x79, 0x61, 0x55, 0x64, 0x6c, 0x38, 0x64, 0x43, 0x63, 0x74, 0x62, 0x65, 0x64, 0x6a, 0x59, 0x73, 0x53, 0x63, 0x66, 0x63, 0x41, 0x65, 0x31, 0x56, 0x52, 0x49, 0x41, 0x73, 0x71, 0x59, 0x37, 0x72, 0x74, 0x51, 0x4e, 0x43, 0x38, 0x4b, 0x54, 0x7a, 0x47, 0x49, 0x7a, 0x4c, 0x58, 0x4b, 0x7a, 0xa4, 0x61, 0x6d, 0x72, 0x68, 0x72, 0x68, 0x58, 0x44, 0x70, 0x44, 0x72, 0x56, 0x72, 0x51, 0x72, 0x4c, 0x42, 0x59, 0x54, 0x4b, 0x4a, 0x62, 0x4b, 0x74, 0x65, 0x58, 0x4d, 0x79, 0x35, 0x54, 0x64, 0x56, 0x35, 0x48, 0x6e, 0x63, 0x7a, 0x55, 0x6d, 0x67, 0x6c, 0x7a, 0x70, 0x44, 0x4b, 0x4b, 0x4a, 0x6b, 0x50, 0x4f, 0x51, 0x62, 0x32, 0x73, 0x78, 0x55, 0x57, 0x51, 0x6a, 0x6c, 0x48, 0x48, 0x71, 0x54, 0x53, 0x32, 0x4b, 0x38, 0x56, 0x53, 0x71, 0x70, 0x78, 0x38, 0x73, 0x30, 0x31, 0x65, 0x37, 0x6b, 0x46, 0x6e, 0x76, 0x53, 0x6b, 0x6c, 0x5f, 0x6f, 0x66, 0x49, 0x34, 0x63, 0x5a, 0x48, 0x72, 0x65, 0x56, 0x47, 0x52, 0x63, 0x47, 0x6a, 0x46, 0x63, 0x79, 0x37, 0x69, 0x50, 0x6f, 0x64, 0x51, 0x73, 0x46, 0x54, 0x4b, 0x5f, 0x73, 0x41, 0x62, 0x33, 0x49, 0x7a, 0x72, 0x71, 0x61, 0x33, 0x64, 0x6f, 0x31, 0x54, 0x78, 0x2d, 0x36, 0x36, 0x5f, 0x79, 0x62, 0x68, 0x64, 0x66, 0x42, 0x4c, 0x67, 0x54, 0x31, 0x7a, 0x46, 0x30, 0x76, 0x32, 0x70, 0x65, 0x56, 0x4d, 0x6a, 0x49, 0x6e, 0x52, 0x35, 0x36, 0x75, 0x67, 0x44, 0x4e, 0x4b, 0x36, 0x6c, 0x75, 0x4d, 0x76, 0x67, 0x34, 0x69, 0x54, 0x6c, 0x75, 0x44, 0xa5, 0x61, 0x51, 0x69, 0x34, 0x6b, 0x64, 0x6f, 0x48, 0x65, 0x54, 0x4c, 0x68, 0x63, 0x6b, 0x4a, 0x39, 0x63, 0x63, 0x68, 0x63, 0x67, 0x71, 0x33, 0x55, 0x34, 0x6f, 0x59, 0x6e, 0x68, 0x4a, 0x78, 0x42, 0x66, 0x4f, 0x58, 0x32, 0x46, 0x64, 0x44, 0x70, 0x57, 0x78, 0x64, 0x66, 0x35, 0x45, 0x65, 0x61, 0x6b, 0x6c, 0x6f, 0x49, 0x4d, 0x51, 0x55, 0x39, 0x6d, 0x6c, 0x76, 0x43, 0x46, 0x59, 0x68, 0x54, 0x30, 0x36, 0x44, 0x61, 0x48, 0x47, 0x31, 0x78, 0x47, 0x45, 0x36, 0x73, 0x67, 0x76, 0x79, 0x36, 0x4a, 0x5a, 0x78, 0x75, 0x6b, 0x50, 0x6f, 0x70, 0x38, 0x37, 0x71, 0x69, 0x62, 0x4f, 0x50, 0x47, 0x72, 0x31, 0x45, 0x74, 0x7a, 0x46, 0x66, 0x68, 0x4d, 0x33, 0x49, 0x31, 0x38, 0x56, 0x38, 0x53, 0x55, 0x63, 0x75, 0x43, 0x71, 0x4f, 0x57, 0x51, 0x46, 0x43, 0x49, 0x6d, 0x6e, 0x59, 0x72, 0x79, 0x30, 0x7a, 0x76, 0x7a, 0x62, 0x51, 0x62, 0x35, 0x57, 0x4a, 0x6b, 0x6e, 0x31, 0x67, 0x51, 0x71, 0x6f, 0x31, 0x4a, 0x74, 0x50, 0x35, 0x4c, 0x4a, 0x35, 0x50, 0x5f, 0x37, 0x5f, 0x59, 0x52, 0x42, 0xa5, 0x61, 0x54, 0x60, 0x68, 0x6d, 0x44, 0x41, 0x30, 0x7a, 0x6f, 0x6e, 0x56, 0x68, 0x68, 0x49, 0x55, 0x66, 0x73, 0x55, 0x62, 0x54, 0x69, 0x31, 0x30, 0x77, 0x36, 0x59, 0x39, 0x76, 0x55, 0x75, 0xa1, 0x61, 0x62, 0x64, 0x58, 0x73, 0x55, 0x52, 0x61, 0x62, 0x6b, 0x38, 0x41, 0x4e, 0x33, 0x4b, 0x39, 0x62, 0x48, 0x4f, 0x6f, 0x4c, 0x61, 0x58, 0xa1, 0x61, 0x54, 0x6a, 0x55, 0x4f, 0x4b, 0x67, 0x34, 0x65, 0x75, 0x43, 0x48, 0x4d, 0x68, 0x38, 0x31, 0x48, 0x59, 0x62, 0x45, 0x4d, 0x72, 0x78, 0x50, 0x4e, 0x73, 0x76, 0x53, 0x32, 0x6e, 0x6e, 0x49, 0x4f, 0x73, 0x6f, 0x5f, 0x53, 0x53, 0x43, 0x56, 0x51, 0x76, 0x62, 0x7a, 0x71, 0x46, 0x65, 0x48, 0x42, 0x35, 0x4e, 0x55, 0x64, 0x32, 0x73, 0x63, 0x47, 0x4b, 0x4d, 0x71, 0x6b, 0x57, 0x65, 0x56, 0x36, 0x4e, 0x42, 0x64, 0x4e, 0x71, 0x56, 0x5a, 0x54, 0x34, 0x35, 0x48, 0x30, 0x44, 0x47, 0x4e, 0x5a, 0x67, 0x6a, 0x63, 0x65, 0x53, 0x47, 0x57, 0x50, 0x35, 0x75, 0x78, 0x79, 0x6a, 0x48, 0x33, 0x4f, 0x4a, 0x72, 0x31, 0x37, 0x32, 0x51, 0x47, 0x68, 0x6b, 0x6a, 0x64, 0x6e, 0x65, 0x51, 0x36, 0x35, 0x78, 0x30, 0x56, 0x53, 0x51, 0x4b, 0x6b, 0x4d, 0x71, 0x32, 0x75, 0x64, 0x55, 0x52, 0x74, 0x38, 0x37, 0x5a, 0x6f, 0x53, 0x68, 0x58, 0x76, 0x75, 0x51, 0x55, 0x6c, 0x52, 0x6a, 0x55, 0x62, 0x44, 0x4a, 0x71, 0x71, 0x51, 0x5f, 0x42, 0x79, 0x36, 0x38, 0x63, 0x69, 0x37, 0x63, 0x5a, 0x79, 0x41, 0x35, 0x41, 0x78, 0x1e, 0x31, 0x5a, 0x65, 0x5f, 0x4e, 0x6a, 0x39, 0x76, 0x5f, 0x5f, 0x6c, 0x46, 0x59, 0x65, 0x74, 0x69, 0x70, 0x39, 0x47, 0x41, 0x42, 0x69, 0x63, 0x57, 0x67, 0x47, 0x63, 0x6e, 0x4f, 0x68, 0x78, 0x36, 0x4f, 0x45, 0x64, 0x5f, 0x34, 0x36, 0x47, 0x77, 0x44, 0x76, 0x34, 0x55, 0x6b, 0x34, 0x78, 0x51, 0x4e, 0x78, 0x33, 0x58, 0x63, 0x74, 0x43, 0x4e, 0x32, 0x50, 0x31, 0x61, 0x62, 0x79, 0x36, 0x44, 0x6b, 0x48, 0x4d, 0x53, 0x32, 0x35, 0x32, 0x61, 0x74, 0x46, 0x33, 0x4d, 0x75, 0x6b, 0x53, 0x38, 0x4d, 0x4a, 0x4a, 0x66, 0x54, 0x34, 0x73, 0x58, 0x30, 0x34, 0x44, 0x4c, 0x64, 0x6c, 0x6e, 0x4f, 0x46, 0x44, 0x66, 0x76, 0x6a, 0x36, 0x47, 0x38, 0x4d, 0x5f, 0x76, 0x46, 0x6e, 0x67, 0x30, 0x37, 0x69, 0x62, 0x6c, 0x41, 0x6f, 0x65, 0x57, 0x50, 0x56, 0x46, 0x6d, 0x76, 0x47, 0x41, 0x54, 0x79, 0x73, 0x68, 0x70, 0x31, 0x4b, 0x45, 0x6e, 0x4f, 0x44, 0x4a, 0x78, 0x37, 0x48, 0x35, 0x45, 0x6d, 0x4b, 0x43, 0x46, 0x76, 0x4d, 0x4c, 0x78, 0x62, 0x56, 0x32, 0x6a, 0x54, 0x68, 0x54, 0x34, 0x41, 0x57, 0x6d, 0x67, 0x57, 0x33, 0x6e, 0x64, 0x32, 0x71, 0x34, 0x74, 0x44, 0x59, 0x6a, 0x54, 0x57, 0x6c, 0x31, 0x56, 0x57, 0x72, 0x7a, 0x59, 0x4a, 0x61, 0x31, 0x6b, 0x66, 0x69, 0x36, 0x65, 0x58, 0x51, 0x4d, 0x75, 0x78, 0x19, 0x48, 0x57, 0x4b, 0x62, 0x31, 0x72, 0x55, 0x79, 0x4e, 0x55, 0x61, 0x65, 0x79, 0x71, 0x45, 0x67, 0x55, 0x72, 0x71, 0x58, 0x6b, 0x74, 0x52, 0x34, 0x4c, 0x78, 0x1b, 0x7a, 0x4f, 0x42, 0x50, 0x7a, 0x30, 0x58, 0x37, 0x65, 0x4c, 0x6d, 0x6e, 0x38, 0x58, 0x59, 0x52, 0x69, 0x65, 0x52, 0x56, 0x67, 0x35, 0x76, 0x4c, 0x71, 0x48, 0x74, 0x78, 0x24, 0x4a, 0x69, 0x50, 0x4f, 0x6d, 0x72, 0x7a, 0x58, 0x68, 0x45, 0x72, 0x5f, 0x73, 0x4b, 0x43, 0x45, 0x7a, 0x54, 0x33, 0x75, 0x79, 0x73, 0x6e, 0x4d, 0x5a, 0x73, 0x6b, 0x45, 0x76, 0x77, 0x78, 0x61, 0x32, 0x73, 0x77, 0x73, 0xa4, 0x61, 0x38, 0x65, 0x4f, 0x4a, 0x6e, 0x4c, 0x52, 0x64, 0x6c, 0x68, 0x31, 0x57, 0x65, 0x7a, 0x6a, 0x38, 0x39, 0x37, 0x62, 0x30, 0x4a, 0x64, 0x57, 0x70, 0x43, 0x6a, 0x64, 0x6b, 0x5f, 0x4a, 0x6f, 0x6b, 0x63, 0x35, 0x77, 0x6d, 0x67, 0x47, 0x33, 0x42, 0x76, 0x4d, 0x72, 0x78, 0x18, 0x6a, 0x42, 0x5f, 0x6f, 0x41, 0x30, 0x77, 0x72, 0x4b, 0x41, 0x4d, 0x6c, 0x34, 0x33, 0x35, 0x59, 0x4e, 0x74, 0x67, 0x74, 0x42, 0x6e, 0x77, 0x5f, 0x78, 0x19, 0x6c, 0x43, 0x5a, 0x48, 0x7a, 0x75, 0x59, 0x68, 0x76, 0x35, 0x73, 0x6a, 0x71, 0x43, 0x50, 0x70, 0x59, 0x72, 0x36, 0x6f, 0x71, 0x76, 0x4c, 0x71, 0x4f, 0x69, 0x49, 0x32, 0x41, 0x79, 0x45, 0x4b, 0x75, 0x4e, 0x42, 0x78, 0x44, 0x69, 0x68, 0x66, 0x57, 0x41, 0x51, 0x33, 0x56, 0x56, 0x65, 0x39, 0x43, 0x38, 0x7a, 0x66, 0x72, 0x52, 0x43, 0x72, 0x4f, 0x4f, 0x42, 0x35, 0x4f, 0x31, 0x68, 0x4d, 0x5f, 0x44, 0x54, 0x78, 0x77, 0x50, 0x33, 0x6f, 0x76, 0x72, 0x64, 0x46, 0x32, 0x79, 0x5f, 0x63, 0x61, 0x31, 0x34, 0x6f, 0x32, 0x32, 0x72, 0x74, 0x55, 0x65, 0x50, 0x55, 0x71, 0x47, 0x6c, 0x51, 0x43, 0x4f, 0x61, 0x7a, 0x70, 0x4c, 0x64, 0x72, 0x59, 0x78, 0x18, 0x6a, 0x71, 0x50, 0x47, 0x53, 0x54, 0x37, 0x55, 0x38, 0x59, 0x39, 0x61, 0x54, 0x4f, 0x35, 0x78, 0x36, 0x37, 0x71, 0x34, 0x7a, 0x48, 0x72, 0x4c, 0x78, 0x37, 0x54, 0x65, 0x79, 0x72, 0x37, 0x71, 0x75, 0x47, 0x31, 0x45, 0x72, 0x7a, 0x4e, 0x35, 0x68, 0x64, 0x33, 0x75, 0x6a, 0x74, 0x77, 0x72, 0x4a, 0x4d, 0x33, 0x42, 0x79, 0x66, 0x72, 0x54, 0x72, 0x48, 0x4c, 0x6c, 0x37, 0x65, 0x6b, 0x5f, 0x55, 0x66, 0x61, 0x6d, 0x65, 0x38, 0x42, 0x43, 0x71, 0x63, 0x66, 0x59, 0x71, 0x62, 0x54, 0x79, 0x4e, 0x6c, 0x45, 0x53, 0x6d, 0x6c, 0x32, 0x76, 0x58, 0x6d, 0x53, 0x62, 0x34, 0x65, 0x78, 0x3d, 0x61, 0x32, 0x63, 0x6b, 0x66, 0x32, 0x58, 0x6c, 0x30, 0x7a, 0x64, 0x64, 0x63, 0x6d, 0x68, 0x78, 0x73, 0x7a, 0x57, 0x44, 0x77, 0x78, 0x69, 0x4e, 0x5a, 0x46, 0x33, 0x50, 0x66, 0x4e, 0x38, 0x62, 0x66, 0x4c, 0x45, 0x61, 0x58, 0x6c, 0x37, 0x61, 0x6d, 0x58, 0x36, 0x63, 0x36, 0x46, 0x46, 0x45, 0x5f, 0x67, 0x49, 0x61, 0x61, 0x4f, 0x42, 0x37, 0x5f, 0x47, 0x4c, 0x45, 0x43, 0x75, 0x75, 0x4f, 0x54, 0x52, 0x47, 0x5f, 0x33, 0x42, 0x54, 0x34, 0x6a, 0x4e, 0x36, 0x34, 0x70, 0x48, 0x76, 0x5f, 0x55, 0x57, 0x6f, 0xa4, 0x61, 0x44, 0x68, 0x53, 0x71, 0x79, 0x6d, 0x65, 0x36, 0x61, 0x6d, 0x62, 0x66, 0x77, 0x69, 0x77, 0x63, 0x45, 0x51, 0x4a, 0x73, 0x5f, 0x76, 0x5f, 0x64, 0x61, 0x36, 0x6d, 0x35, 0x67, 0x6f, 0x69, 0x54, 0x34, 0x4c, 0x58, 0x31, 0x62, 0x63, 0x31, 0x6f, 0x75, 0x49, 0x72, 0x72, 0x76, 0x63, 0x33, 0x37, 0x54, 0x72, 0x73, 0x48, 0x50, 0x62, 0x57, 0x78, 0x1a, 0x51, 0x41, 0x65, 0x5f, 0x56, 0x74, 0x6d, 0x77, 0x65, 0x68, 0x4f, 0x73, 0x32, 0x35, 0x6d, 0x70, 0x66, 0x67, 0x6c, 0x59, 0x38, 0x49, 0x59, 0x45, 0x56, 0x75, 0x78, 0x22, 0x54, 0x58, 0x54, 0x37, 0x66, 0x5a, 0x48, 0x37, 0x5a, 0x41, 0x69, 0x4d, 0x5f, 0x79, 0x51, 0x77, 0x34, 0x45, 0x33, 0x6b, 0x66, 0x66, 0x67, 0x35, 0x68, 0x32, 0x4f, 0x57, 0x50, 0x37, 0x45, 0x31, 0x65, 0x5a, 0x6e, 0x33, 0x33, 0x66, 0x50, 0x63, 0x43, 0x78, 0x39, 0x54, 0x61, 0x4b, 0x38, 0x45, 0x67, 0xa5, 0x61, 0x6d, 0x6c, 0x71, 0x49, 0x50, 0x4e, 0x4d, 0x6b, 0x65, 0x49, 0x6c, 0x4e, 0x61, 0x46, 0x63, 0x4d, 0x57, 0x6b, 0x62, 0x51, 0x5a, 0x64, 0x59, 0x4a, 0x6e, 0x5a, 0x63, 0x6e, 0x32, 0x4a, 0x63, 0x4e, 0x66, 0x36, 0x63, 0x42, 0x67, 0x72, 0x64, 0x73, 0x36, 0x51, 0x67, 0x6b, 0x65, 0x78, 0x59, 0x4d, 0x52, 0x4d, 0x6c, 0x78, 0x41, 0x50, 0x59, 0x65, 0x48, 0x65, 0x47, 0x77, 0x52, 0x78, 0x45, 0x63, 0x44, 0x33, 0x32, 0x30, 0x45, 0x4b, 0x38, 0x65, 0x70, 0x37, 0x37, 0x46, 0x6f, 0x52, 0x67, 0x43, 0x6e, 0x6e, 0x69, 0x66, 0x47, 0x48, 0x5a, 0x6a, 0x4f, 0x4c, 0x70, 0x50, 0x32, 0x72, 0x52, 0x56, 0x31, 0x35, 0x63, 0x61, 0x31, 0x50, 0x37, 0x43, 0x44, 0x4b, 0x41, 0x64, 0x66, 0x65, 0x42, 0x5a, 0x53, 0x35, 0x72, 0x68, 0x41, 0x34, 0x6c, 0x48, 0x30, 0x6d, 0x54, 0x78, 0x5f, 0x6f, 0x48, 0x72, 0x5a, 0x76, 0x4c, 0x49, 0x67, 0x32, 0x51, 0x78, 0x46, 0x4d, 0x46, 0x38, 0xa7, 0x61, 0x79, 0x62, 0x5a, 0x35, 0x69, 0x6b, 0x30, 0x6f, 0x74, 0x77, 0x6d, 0x36, 0x32, 0x79, 0x69, 0x32, 0x79, 0x74, 0x78, 0x4f, 0x77, 0x42, 0x71, 0x50, 0x66, 0x64, 0x35, 0x39, 0x66, 0x65, 0x4b, 0x6e, 0x31, 0x79, 0x7a, 0x41, 0x70, 0x46, 0x65, 0x50, 0x71, 0x5a, 0x71, 0x36, 0x4c, 0x7a, 0x63, 0x62, 0x4c, 0x6f, 0x62, 0x4a, 0x35, 0x64, 0x68, 0x56, 0x4b, 0x6e, 0x63, 0x6e, 0x31, 0x4d, 0x67, 0x50, 0x78, 0x43, 0x4c, 0x45, 0x73, 0x65, 0x6b, 0x63, 0x36, 0x75, 0x57, 0x45, 0x6e, 0x31, 0x30, 0x71, 0x44, 0x71, 0x64, 0x78, 0x6c, 0x78, 0x4c, 0x6a, 0x6e, 0x4a, 0x65, 0x56, 0x4d, 0x55, 0x77, 0x6b, 0x55, 0x4e, 0x78, 0x22, 0x70, 0x79, 0x67, 0x72, 0x4b, 0x5f, 0x4a, 0x46, 0x6c, 0x44, 0x66, 0x4d, 0x57, 0x73, 0x54, 0x50, 0x70, 0x70, 0x57, 0x37, 0x4c, 0x5f, 0x6e, 0x64, 0x68, 0x61, 0x51, 0x4d, 0x6b, 0x49, 0x67, 0x72, 0x6f, 0x59, 0xa6, 0x61, 0x6c, 0x62, 0x74, 0x6c, 0x61, 0x51, 0x6b, 0x35, 0x58, 0x46, 0x57, 0x65, 0x79, 0x6d, 0x39, 0x55, 0x42, 0x38, 0x65, 0x58, 0x59, 0x69, 0x6f, 0x5a, 0x6e, 0x74, 0x68, 0x46, 0x70, 0x6f, 0x51, 0x6a, 0x4d, 0x52, 0x4c, 0x36, 0x48, 0x6a, 0x71, 0x61, 0x52, 0xa1, 0x61, 0x43, 0x63, 0x69, 0x71, 0x6b, 0x68, 0x30, 0x32, 0x33, 0x35, 0x4b, 0x41, 0x58, 0x4c, 0x69, 0x65, 0x74, 0x53, 0x64, 0x53, 0x34, 0x4d, 0x4c, 0x46, 0x63, 0x6d, 0x6f, 0x77, 0x6b, 0x43, 0x79, 0x7a, 0x36, 0x34, 0x38, 0x48, 0x41, 0x44, 0x4b, 0x46, 0x77, 0x78, 0x71, 0x48, 0x43, 0x5f, 0x30, 0x61, 0x6a, 0x53, 0x74, 0x68, 0x41, 0x78, 0x79, 0x6b, 0x4a, 0x46, 0x6e, 0x6f, 0x35, 0x57, 0x4c, 0x74, 0x78, 0x25, 0x6e, 0x47, 0x62, 0x6a, 0x51, 0x66, 0x4a, 0x45, 0x35, 0x44, 0x67, 0x31, 0x5f, 0x63, 0x47, 0x77, 0x5a, 0x7a, 0x63, 0x34, 0x6b, 0x5a, 0x62, 0x4b, 0x53, 0x6e, 0x6e, 0x30, 0x61, 0x6b, 0x75, 0x67, 0x5a, 0x4e, 0x45, 0x65, 0x44, 0x78, 0x33, 0x48, 0x4b, 0x73, 0x75, 0x43, 0x72, 0x5f, 0x36, 0x39, 0x6a, 0x6d, 0x5a, 0x4c, 0x54, 0x34, 0x59, 0x77, 0x32, 0x5a, 0x66, 0x56, 0x36, 0x69, 0x35, 0x47, 0x52, 0x67, 0x79, 0x71, 0x44, 0x4b, 0x75, 0x43, 0x56, 0x63, 0x46, 0x4d, 0x48, 0x32, 0x31, 0x41, 0x78, 0x79, 0x6e, 0x30, 0x35, 0x41, 0x69, 0x47, 0x52, 0x47, 0x78, 0x42, 0x59, 0x64, 0x68, 0x32, 0x4e, 0x68, 0x63, 0x6a, 0x4e, 0x5a, 0x61, 0x6e, 0x67, 0x46, 0x65, 0x4e, 0x32, 0x74, 0x58, 0x71, 0x7a, 0x34, 0x71, 0x5f, 0x48, 0x4a, 0x56, 0x69, 0x4a, 0x74, 0x58, 0x77, 0x54, 0x34, 0x51, 0x77, 0x76, 0x50, 0x32, 0x48, 0x76, 0x4d, 0x38, 0x32, 0x76, 0x61, 0x51, 0x42, 0x54, 0x6a, 0x4a, 0x34, 0x41, 0x48, 0x6e, 0x65, 0x66, 0x73, 0x70, 0x43, 0x72, 0x74, 0x62, 0x62, 0x68, 0x50, 0x78, 0x1c, 0x44, 0x4a, 0x4f, 0x54, 0x5a, 0x52, 0x54, 0x79, 0x33, 0x6a, 0x53, 0x7a, 0x48, 0x44, 0x34, 0x53, 0x66, 0x73, 0x69, 0x6d, 0x6f, 0x38, 0x74, 0x58, 0x72, 0x33, 0x5a, 0x75, 0x78, 0x43, 0x50, 0x68, 0x73, 0x6e, 0x4a, 0x43, 0x49, 0x42, 0x61, 0x73, 0x6f, 0x30, 0x7a, 0x66, 0x7a, 0x70, 0x5a, 0x58, 0x53, 0x71, 0x79, 0x33, 0x70, 0x4d, 0x72, 0x37, 0x49, 0x44, 0x71, 0x38, 0x52, 0x6f, 0x4e, 0x52, 0x4d, 0x42, 0x51, 0x48, 0x52, 0x6c, 0x4d, 0x48, 0x57, 0x69, 0x30, 0x38, 0x71, 0x66, 0x67, 0x78, 0x72, 0x4c, 0x57, 0x4d, 0x35, 0x65, 0x7a, 0x77, 0x6c, 0x36, 0x58, 0x47, 0x32, 0x5a, 0x37, 0x62, 0x44, 0x70, 0x49, 0x74, 0x61, 0x54, 0x61, 0x35, 0x59, 0x69, 0x6f, 0x76, 0x41, 0x7a, 0x75, 0x74, 0x59, 0x33, 0x78, 0x48, 0x49, 0x4a, 0x66, 0x67, 0x53, 0x32, 0x52, 0x65, 0x70, 0x48, 0x77, 0x5a, 0x6a, 0x6c, 0x59, 0x6e, 0x75, 0x4e, 0x70, 0x75, 0x66, 0x6b, 0x72, 0x7a, 0x6c, 0x65, 0x75, 0x39, 0x36, 0x4f, 0x6d, 0x48, 0x73, 0x5a, 0x4e, 0x58, 0x4d, 0x6a, 0x6c, 0x38, 0x78, 0x71, 0x33, 0x79, 0x45, 0x34, 0x66, 0x6a, 0x44, 0x65, 0x4e, 0x54, 0x64, 0x73, 0x79, 0x47, 0x38, 0x77, 0x65, 0x31, 0x4c, 0x31, 0x6d, 0x51, 0x42, 0x71, 0x4e, 0x75, 0x7a, 0x38, 0x73, 0x5a, 0x63, 0x56, 0x31, 0x50, 0x78, 0x2f, 0x38, 0x4a, 0x69, 0x76, 0x53, 0x34, 0x4b, 0x4d, 0x54, 0x53, 0x6e, 0x58, 0x6f, 0x38, 0x33, 0x61, 0x67, 0x39, 0x66, 0x69, 0x44, 0x33, 0x4c, 0x42, 0x44, 0x4e, 0x4d, 0x6f, 0x72, 0x32, 0x6c, 0x53, 0x71, 0x44, 0x70, 0x4e, 0x73, 0x68, 0x36, 0x30, 0x41, 0x78, 0x71, 0x6b, 0x68, 0x67, 0x31, 0x78, 0x2f, 0x6e, 0x59, 0x30, 0x64, 0x30, 0x55, 0x7a, 0x74, 0x53, 0x32, 0x68, 0x48, 0x56, 0x4a, 0x77, 0x4f, 0x51, 0x4c, 0x39, 0x79, 0x47, 0x4c, 0x77, 0x31, 0x57, 0x66, 0x33, 0x55, 0x36, 0x73, 0x37, 0x49, 0x31, 0x54, 0x7a, 0x63, 0x63, 0x52, 0x32, 0x32, 0x72, 0x42, 0x72, 0x68, 0x46, 0x5a, 0x37, 0x78, 0x3e, 0x67, 0x39, 0x44, 0x48, 0x54, 0x48, 0x59, 0x62, 0x43, 0x4a, 0x70, 0x50, 0x34, 0x38, 0x56, 0x73, 0x62, 0x4f, 0x73, 0x69, 0x70, 0x70, 0x74, 0x74, 0x54, 0x75, 0x66, 0x4c, 0x74, 0x32, 0x57, 0x49, 0x53, 0x77, 0x6a, 0x42, 0x6c, 0x33, 0x6c, 0x65, 0x49, 0x55, 0x44, 0x43, 0x6a, 0x79, 0x5f, 0x6d, 0x56, 0x59, 0x56, 0x4e, 0x64, 0x64, 0x66, 0x68, 0x58, 0x30, 0x56, 0x4a, 0x4b, 0x64, 0x67, 0x5f, 0x76, 0x7a, 0x77, 0x52, 0x35, 0x6e, 0x78, 0x59, 0x55, 0x4e, 0x72, 0x52, 0x4d, 0x54, 0x68, 0x46, 0x4d, 0x36, 0x65, 0x5a, 0x5a, 0x66, 0x6c, 0x58, 0x36, 0x77, 0x31, 0x51, 0x5f, 0x49, 0x65, 0x30, 0x79, 0x6f, 0x5f, 0x65, 0x45, 0x46, 0x4c, 0x4a, 0x4a, 0x75, 0x37, 0x33, 0x34, 0x31, 0x79, 0x79, 0x49, 0x35, 0x64, 0x59, 0x4c, 0x30, 0x46, 0x62, 0x66, 0x68, 0x35, 0x6a, 0x46, 0x68, 0x71, 0x4e, 0x6c, 0x6e, 0x52, 0x4f, 0x61, 0x37, 0x53, 0x30, 0x64, 0x66, 0x31, 0x46, 0x34, 0x4f, 0x47, 0x66, 0x4b, 0x56, 0x73, 0x4d, 0x56, 0x36, 0x37, 0x52, 0x6b, 0x39, 0x6c, 0x59, 0x79, 0x38, 0x56, 0x44, 0x30, 0x68, 0x49, 0x42, 0x79, 0x46, 0x76, 0x63, 0x48, 0x4d, 0x78, 0x1f, 0x78, 0x45, 0x71, 0x46, 0x52, 0x53, 0x42, 0x54, 0x72, 0x37, 0x4e, 0x6b, 0x53, 0x33, 0x4d, 0x6f, 0x76, 0x30, 0x67, 0x77, 0x6b, 0x46, 0x55, 0x4a, 0x33, 0x50, 0x74, 0x4c, 0x45, 0x6c, 0x5a, 0x6e, 0x59, 0x35, 0x63, 0x64, 0x66, 0x44, 0x30, 0x6b, 0x54, 0x66, 0x46, 0x6f, 0x71, 0x4d, 0x78, 0x52, 0x69, 0x6b, 0x67, 0x52, 0x76, 0x5f, 0x62, 0x51, 0x54, 0x76, 0x75, 0x47, 0x7a, 0x30, 0x70, 0x45, 0x63, 0x5f, 0x62, 0x54, 0x66, 0x58, 0x79, 0x74, 0x39, 0x5a, 0x74, 0x39, 0x73, 0x65, 0x59, 0x74, 0x6e, 0x75, 0x6e, 0x43, 0x39, 0x55, 0x38, 0x4c, 0x54, 0x37, 0x32, 0x69, 0x48, 0x33, 0x36, 0x4b, 0x61, 0x54, 0x78, 0x42, 0x38, 0x36, 0x65, 0x56, 0x35, 0x44, 0x4b, 0x4b, 0x6f, 0x74, 0x6d, 0x63, 0x63, 0x4e, 0x79, 0x6d, 0x63, 0x4e, 0x57, 0x32, 0x56, 0x76, 0x33, 0x55, 0x46, 0x6c, 0x6d, 0x73, 0x48, 0x68, 0x72, 0x36, 0x31, 0x5f, 0x61, 0x4c, 0x49, 0x56, 0x64, 0x5a, 0x72, 0x6a, 0x54, 0x51, 0x66, 0x52, 0x55, 0x41, 0x30, 0x78, 0x2a, 0x38, 0x55, 0x71, 0x51, 0x77, 0x52, 0x35, 0x70, 0x33, 0x58, 0x32, 0x75, 0x7a, 0x4b, 0x49, 0x37, 0x4e, 0x4e, 0x65, 0x49, 0x6c, 0x79, 0x71, 0x77, 0x72, 0x6c, 0x53, 0x31, 0x4c, 0x68, 0x42, 0x4f, 0x72, 0x6d, 0x4f, 0x5a, 0x53, 0x55, 0x75, 0x33, 0x47, 0x30, 0x78, 0x1d, 0x69, 0x57, 0x34, 0x6d, 0x49, 0x51, 0x35, 0x78, 0x49, 0x5f, 0x75, 0x79, 0x32, 0x33, 0x31, 0x68, 0x77, 0x66, 0x62, 0x50, 0x49, 0x53, 0x52, 0x32, 0x4a, 0x70, 0x30, 0x61, 0x57, 0xa2, 0x61, 0x7a, 0x70, 0x78, 0x33, 0x7a, 0x70, 0x31, 0x74, 0x72, 0x65, 0x6a, 0x58, 0x45, 0x6b, 0x39, 0x71, 0x33, 0x79, 0x67, 0x51, 0x49, 0x45, 0x37, 0x42, 0x67, 0x63, 0x67, 0x61, 0x50, 0x66, 0x36, 0x79, 0x66, 0x33, 0x70, 0x62, 0x41, 0x79, 0x62, 0x46, 0x36, 0x47, 0x52, 0x6e, 0x59, 0x47, 0x69, 0x6c, 0x65, 0x62, 0x43, 0xa7, 0x61, 0x4a, 0x62, 0x53, 0x6a, 0x64, 0x59, 0x79, 0x4b, 0x36, 0x6e, 0x56, 0x4f, 0x66, 0x6f, 0x35, 0x56, 0x6c, 0x68, 0x61, 0x32, 0x43, 0x78, 0x37, 0x75, 0x64, 0x38, 0x6b, 0x79, 0x68, 0x64, 0x36, 0x62, 0x78, 0x6d, 0x63, 0x61, 0x51, 0x4a, 0x62, 0x33, 0x37, 0x63, 0x66, 0x38, 0x34, 0x66, 0x37, 0x47, 0x39, 0x6f, 0x7a, 0x61, 0x61, 0x53, 0x6d, 0x59, 0x48, 0x49, 0x78, 0x51, 0x63, 0x6b, 0x6d, 0x39, 0x64, 0x77, 0x77, 0x68, 0x65, 0x31, 0x4f, 0x79, 0x38, 0x38, 0x66, 0x6c, 0x39, 0x30, 0x66, 0x52, 0x32, 0x62, 0x65, 0x77, 0x78, 0x1d, 0x62, 0x43, 0x4b, 0x72, 0x42, 0x6e, 0x67, 0x5a, 0x45, 0x31, 0x6a, 0x69, 0x69, 0x62, 0x31, 0x67, 0x34, 0x55, 0x6b, 0x69, 0x4f, 0x6e, 0x34, 0x6f, 0x53, 0x65, 0x56, 0x58, 0x5f, 0x78, 0x19, 0x52, 0x65, 0x50, 0x32, 0x51, 0x4d, 0x56, 0x43, 0x4a, 0x66, 0x73, 0x77, 0x59, 0x56, 0x4d, 0x30, 0x31, 0x59, 0x57, 0x6a, 0x50, 0x4a, 0x63, 0x65, 0x32, 0x78, 0x29, 0x6e, 0x6f, 0x45, 0x79, 0x70, 0x38, 0x36, 0x45, 0x74, 0x49, 0x61, 0x53, 0x54, 0x59, 0x54, 0x4b, 0x57, 0x5a, 0x79, 0x44, 0x52, 0x33, 0x76, 0x4d, 0x42, 0x72, 0x31, 0x31, 0x6d, 0x45, 0x48, 0x72, 0x6c, 0x43, 0x4d, 0x4e, 0x66, 0x70, 0x74, 0x47, 0x4e, 0x78, 0x1a, 0x38, 0x37, 0x6e, 0x78, 0x79, 0x35, 0x68, 0x54, 0x38, 0x45, 0x50, 0x4d, 0x45, 0x75, 0x61, 0x69, 0x7a, 0x6b, 0x4a, 0x39, 0x7a, 0x50, 0x47, 0x39, 0x43, 0x64, 0x78, 0x4f, 0x5a, 0x72, 0x6b, 0x75, 0x69, 0x46, 0x58, 0x77, 0x53, 0x43, 0x4a, 0x4c, 0x70, 0x73, 0x70, 0x36, 0x68, 0x4e, 0x39, 0x41, 0x41, 0x78, 0x58, 0x76, 0x5f, 0x46, 0x74, 0x6d, 0x4c, 0x52, 0x63, 0x4f, 0x55, 0x4a, 0x6d, 0x7a, 0x57, 0x56, 0x4e, 0x42, 0x46, 0x55, 0x30, 0x68, 0x37, 0x75, 0x79, 0x5f, 0x75, 0x57, 0x55, 0x38, 0x47, 0x47, 0x34, 0x59, 0x6b, 0x55, 0x31, 0x67, 0x71, 0x32, 0x64, 0x37, 0x32, 0x32, 0x74, 0x53, 0x35, 0x46, 0x64, 0x64, 0x6c, 0x31, 0x50, 0x56, 0x48, 0x62, 0x70, 0x6f, 0x76, 0x72, 0x7a, 0x41, 0x5f, 0x6e, 0x6b, 0x64, 0x38, 0x58, 0x78, 0x6b, 0x34, 0x77, 0x4f, 0x78, 0x25, 0x70, 0x33, 0x71, 0x4f, 0x74, 0x77, 0x53, 0x48, 0x6e, 0x53, 0x49, 0x33, 0x73, 0x6b, 0x74, 0x6d, 0x33, 0x36, 0x48, 0x58, 0x67, 0x68, 0x57, 0x65, 0x4c, 0x65, 0x6b, 0x52, 0x51, 0x37, 0x32, 0x48, 0x73, 0x6f, 0x58, 0x66, 0x54, 0x63, 0x4c, 0x32, 0x38, 0x78, 0x72, 0x7a, 0x35, 0x7a, 0x4f, 0x79, 0x59, 0x58, 0x72, 0x59, 0x37, 0x52, 0x37, 0x52, 0x70, 0x73, 0x6a, 0x47, 0x44, 0x6a, 0x6f, 0x61, 0x38, 0x33, 0x4b, 0x32, 0x44, 0x4a, 0x6b, 0x35, 0x70, 0x48, 0x77, 0x61, 0x43, 0x4b, 0x61, 0x64, 0x66, 0x6d, 0x4f, 0x6b, 0x64, 0x36, 0x63, 0x31, 0x75, 0x51, 0x4b, 0x64, 0x53, 0x59, 0x4a, 0x35, 0x31, 0x64, 0x6d, 0x53, 0x48, 0x53, 0x7a, 0x70, 0x63, 0x69, 0x47, 0x65, 0x4f, 0x68, 0x55, 0x48, 0x49, 0x70, 0x31, 0x44, 0x58, 0x42, 0x31, 0x42, 0x34, 0x45, 0x6f, 0x61, 0x50, 0x57, 0x4c, 0x6e, 0x54, 0x6e, 0x69, 0x4b, 0x30, 0x51, 0x53, 0x68, 0x50, 0x39, 0x6a, 0x48, 0x4b, 0x64, 0x4a, 0x36, 0x31, 0x37, 0x6c, 0x43, 0x56, 0x73, 0x49, 0x45, 0x49, 0x62, 0x54, 0x31, 0x41, 0x6c, 0x49, 0x65, 0x33, 0x36, 0x6f, 0x4f, 0x4c, 0x49, 0x38, 0x70, 0x6f, 0x78, 0xa7, 0x61, 0x62, 0x65, 0x7a, 0x65, 0x6d, 0x37, 0x37, 0x67, 0x54, 0x7a, 0x43, 0x56, 0x4f, 0x78, 0x33, 0x68, 0x66, 0x70, 0x67, 0x46, 0x47, 0x4e, 0x32, 0x6f, 0x6a, 0x79, 0x4f, 0x35, 0x59, 0x45, 0x68, 0x30, 0x76, 0x35, 0x52, 0x6a, 0x32, 0x7a, 0x37, 0x39, 0x78, 0x50, 0x38, 0x4c, 0x43, 0x39, 0x62, 0x31, 0x5f, 0xa1, 0x61, 0x5f, 0x66, 0x69, 0x46, 0x39, 0x75, 0x62, 0x77, 0x67, 0x6e, 0x63, 0x75, 0x32, 0x79, 0x65, 0x58, 0x66, 0x6a, 0x73, 0x6e, 0x66, 0x58, 0x36, 0x62, 0x75, 0x45, 0x63, 0x55, 0x64, 0x6a, 0x69, 0x4e, 0x6d, 0x66, 0x55, 0x68, 0x42, 0x6b, 0x75, 0x7a, 0x69, 0x4d, 0x5a, 0x70, 0x74, 0x77, 0x62, 0x6e, 0x4d, 0x4e, 0x78, 0x22, 0x4f, 0x5a, 0x36, 0x79, 0x5a, 0x51, 0x7a, 0x74, 0x51, 0x73, 0x47, 0x55, 0x71, 0x71, 0x61, 0x45, 0x61, 0x4c, 0x75, 0x55, 0x4d, 0x5f, 0x56, 0x6e, 0x36, 0x53, 0x67, 0x68, 0x6b, 0x46, 0x59, 0x77, 0x63, 0x6c, 0x78, 0x32, 0x49, 0x34, 0x4a, 0x6c, 0x38, 0x39, 0x65, 0x6e, 0x58, 0x63, 0x33, 0x6f, 0x33, 0x70, 0x50, 0x69, 0x7a, 0x6a, 0x50, 0x74, 0x4f, 0x50, 0x75, 0x6a, 0x33, 0x6e, 0x77, 0x4a, 0x63, 0x74, 0x59, 0x49, 0x71, 0x5f, 0x6d, 0x6d, 0x71, 0x54, 0x52, 0x57, 0x65, 0x37, 0x43, 0x41, 0x63, 0x75, 0x73, 0x4a, 0x50, 0x6a, 0x62, 0x43, 0x67, 0xa3, 0x61, 0x7a, 0x60, 0x61, 0x63, 0x6d, 0x38, 0x55, 0x44, 0x50, 0x49, 0x70, 0x73, 0x6d, 0x39, 0x53, 0x51, 0x6d, 0x4c, 0x65, 0x5a, 0x76, 0x4a, 0x31, 0x67, 0x65, 0x30, 0x61, 0x51, 0x6d, 0x6c, 0x67, 0x79, 0x72, 0x46, 0x71, 0x63, 0x71, 0x4f, 0x78, 0x3a, 0x71, 0x42, 0x68, 0x4b, 0x59, 0x48, 0x48, 0x34, 0x33, 0x79, 0x34, 0x49, 0x6d, 0x48, 0x65, 0x4c, 0x72, 0x55, 0x6c, 0x39, 0x37, 0x58, 0x4a, 0x71, 0x64, 0x4a, 0x68, 0x72, 0x73, 0x69, 0x72, 0x75, 0x44, 0x54, 0x33, 0x6d, 0x74, 0x32, 0x6e, 0x4a, 0x31, 0x5f, 0x4b, 0x39, 0x72, 0x6c, 0x6c, 0x72, 0x69, 0x34, 0x6f, 0x79, 0x31, 0x39, 0x77, 0x65, 0x6c, 0x6a, 0x78, 0x1b, 0x6b, 0x6d, 0x76, 0x77, 0x58, 0x4c, 0x51, 0x54, 0x39, 0x5a, 0x64, 0x49, 0x59, 0x42, 0x71, 0x6d, 0x55, 0x37, 0x69, 0x5a, 0x50, 0x52, 0x68, 0x31, 0x4a, 0x44, 0x66, 0x78, 0x44, 0x4a, 0x46, 0x78, 0x33, 0x6e, 0x46, 0x73, 0x51, 0x34, 0x41, 0x62, 0x7a, 0x62, 0x30, 0x61, 0x43, 0x37, 0x69, 0x38, 0x34, 0x37, 0x4b, 0x57, 0x32, 0x56, 0x52, 0x64, 0x74, 0x38, 0x30, 0x38, 0x6f, 0x61, 0x64, 0x56, 0x44, 0x68, 0x38, 0x51, 0x65, 0x4a, 0x50, 0x50, 0x55, 0x6c, 0x61, 0x59, 0x56, 0x35, 0x5f, 0x45, 0x53, 0x52, 0x65, 0x41, 0x35, 0x55, 0x38, 0x6e, 0x51, 0x51, 0x32, 0x56, 0x50, 0x33, 0x61, 0x6c, 0x5f, 0x72, 0x32, 0x69, 0x51, 0x71, 0x76, 0x53, 0x48, 0x67, 0x54, 0x4d, 0x78, 0x76, 0x56, 0x47, 0x52, 0x6a, 0x62, 0x43, 0x78, 0x4c, 0x35, 0x70, 0x65, 0x66, 0x4f, 0x37, 0x72, 0x61, 0x76, 0x69, 0x76, 0x6c, 0x69, 0x75, 0x74, 0x4f, 0x33, 0x42, 0x74, 0x68, 0x77, 0x4c, 0x6a, 0x42, 0x47, 0x57, 0x32, 0x58, 0x31, 0x79, 0x4b, 0x53, 0x77, 0x49, 0x35, 0x34, 0x6d, 0x56, 0x70, 0x64, 0x45, 0x68, 0x55, 0x43, 0x68, 0x35, 0x78, 0x78, 0x52, 0x34, 0x61, 0x58, 0x61, 0x6b, 0x6b, 0x75, 0x68, 0x57, 0x4a, 0x4a, 0x56, 0x6a, 0x79, 0x34, 0x33, 0x4c, 0x42, 0x58, 0x42, 0x32, 0x4a, 0x43, 0x6f, 0x30, 0x48, 0x54, 0x78, 0x19, 0x6b, 0x54, 0x32, 0x62, 0x52, 0x46, 0x51, 0x50, 0x61, 0x5f, 0x77, 0x75, 0x43, 0x46, 0x71, 0x6b, 0x4b, 0x44, 0x35, 0x75, 0x4b, 0x46, 0x78, 0x43, 0x59, 0x78, 0x3e, 0x37, 0x49, 0x53, 0x45, 0x52, 0x79, 0x6f, 0x4a, 0x38, 0x71, 0x48, 0x61, 0x4e, 0x67, 0x6e, 0x79, 0x69, 0x4e, 0x67, 0x76, 0x47, 0x51, 0x50, 0x6a, 0x38, 0x41, 0x67, 0x4a, 0x69, 0x43, 0x5a, 0x53, 0x62, 0x56, 0x62, 0x53, 0x4d, 0x79, 0x67, 0x61, 0x46, 0x78, 0x7a, 0x4a, 0x49, 0x6b, 0x77, 0x56, 0x6e, 0x59, 0x59, 0x7a, 0x63, 0x63, 0x68, 0x65, 0x32, 0x4f, 0x6e, 0x57, 0x50, 0x4c, 0x74, 0x44, 0x5f, 0x4e, 0x6e, 0x62, 0x58, 0x66, 0x39, 0x4b, 0x65, 0x56, 0x64, 0x70, 0x33, 0x76, 0x72, 0x38, 0x65, 0x65, 0x38, 0x78, 0x23, 0x6a, 0x34, 0x71, 0x35, 0x46, 0x4a, 0x75, 0x30, 0x6f, 0x71, 0x68, 0x4f, 0x6b, 0x6f, 0x59, 0x6c, 0x49, 0x69, 0x6c, 0x78, 0x69, 0x53, 0x39, 0x74, 0x36, 0x39, 0x46, 0x49, 0x54, 0x34, 0x4e, 0x49, 0x73, 0x44, 0x63, 0x78, 0x1e, 0x6a, 0x62, 0x79, 0x44, 0x39, 0x77, 0x61, 0x35, 0x6a, 0x47, 0x5f, 0x53, 0x4c, 0x47, 0x49, 0x68, 0x49, 0x72, 0x79, 0x6d, 0x6b, 0x70, 0x68, 0x56, 0x74, 0x61, 0x57, 0x58, 0x6d, 0x67, 0x78, 0x23, 0x4e, 0x48, 0x77, 0x38, 0x36, 0x4a, 0x33, 0x67, 0x63, 0x72, 0x48, 0x33, 0x6f, 0x56, 0x33, 0x56, 0x76, 0x62, 0x47, 0x43, 0x7a, 0x33, 0x35, 0x68, 0x53, 0x6d, 0x4a, 0x6f, 0x30, 0x70, 0x36, 0x4a, 0x38, 0x49, 0x4b, 0x78, 0x21, 0x35, 0x34, 0x45, 0x63, 0x52, 0x65, 0x73, 0x30, 0x64, 0x34, 0x32, 0x4b, 0x76, 0x6c, 0x34, 0x50, 0x51, 0x79, 0x35, 0x59, 0x4d, 0x65, 0x5a, 0x69, 0x42, 0x41, 0x36, 0x79, 0x34, 0x38, 0x37, 0x59, 0x53, 0x78, 0x3f, 0x4e, 0x79, 0x48, 0x5f, 0x56, 0x78, 0x49, 0x58, 0x62, 0x6a, 0x74, 0x64, 0x42, 0x67, 0x62, 0x50, 0x57, 0x30, 0x61, 0x76, 0x55, 0x6f, 0x32, 0x37, 0x79, 0x45, 0x68, 0x4f, 0x4c, 0x4d, 0x48, 0x38, 0x6c, 0x33, 0x6c, 0x70, 0x37, 0x75, 0x75, 0x55, 0x67, 0x79, 0x30, 0x35, 0x6f, 0x5a, 0x4f, 0x70, 0x77, 0x71, 0x50, 0x48, 0x49, 0x5a, 0x65, 0x38, 0x79, 0x6d, 0x54, 0x4a, 0x64, 0x7a, 0x47, 0x78, 0x2d, 0x30, 0x44, 0x5f, 0x46, 0x50, 0x67, 0x64, 0x72, 0x79, 0x30, 0x76, 0x61, 0x6a, 0x46, 0x65, 0x44, 0x47, 0x57, 0x69, 0x4c, 0x5a, 0x79, 0x6e, 0x74, 0x61, 0x45, 0x62, 0x6d, 0x49, 0x33, 0x30, 0x59, 0x58, 0x6f, 0x70, 0x54, 0x4b, 0x54, 0x56, 0x73, 0x6c, 0x33, 0x36, 0x33, 0x39, 0x78, 0x2f, 0x53, 0x37, 0x43, 0x6d, 0x43, 0x6b, 0x78, 0x6c, 0x7a, 0x51, 0x50, 0x39, 0x69, 0x37, 0x51, 0x58, 0x41, 0x56, 0x75, 0x58, 0x54, 0x47, 0x6a, 0x4c, 0x5a, 0x48, 0x64, 0x47, 0x73, 0x48, 0x4c, 0x72, 0x75, 0x70, 0x35, 0x67, 0x61, 0x52, 0x64, 0x35, 0x75, 0x42, 0x55, 0x4a, 0x64, 0x70, 0x55, 0x78, 0x1b, 0x54, 0x31, 0x68, 0x58, 0x4d, 0x78, 0x50, 0x77, 0x55, 0x30, 0x55, 0x55, 0x6b, 0x43, 0x52, 0x37, 0x7a, 0x4a, 0x4e, 0x51, 0x47, 0x5a, 0x53, 0x72, 0x67, 0x37, 0x51, 0x78, 0x4d, 0x37, 0x6a, 0x68, 0x76, 0x55, 0x63, 0x43, 0x48, 0x79, 0x79, 0x4c, 0x78, 0x7a, 0x53, 0x47, 0x69, 0x58, 0x5a, 0x51, 0x73, 0x48, 0x31, 0x7a, 0x67, 0x35, 0x4f, 0x7a, 0x71, 0x5a, 0x43, 0x6f, 0x5f, 0x44, 0x79, 0x67, 0x4d, 0x47, 0x65, 0x39, 0x6d, 0x6b, 0x36, 0x50, 0x72, 0x55, 0x73, 0x62, 0x57, 0x75, 0x4a, 0x6f, 0x6e, 0x44, 0x6c, 0x30, 0x32, 0x39, 0x68, 0x45, 0x6b, 0x58, 0x79, 0x53, 0x61, 0x37, 0x37, 0x6e, 0x4e, 0x6c, 0x74, 0x61, 0x44, 0x59, 0x53, 0x42, 0x4f, 0x6d, 0x69, 0x38, 0x6b, 0x6a, 0x37, 0x77, 0x5f, 0x59, 0x36, 0x6b, 0x78, 0x6d, 0x5f, 0x54, 0x51, 0x46, 0x6b, 0x77, 0x39, 0x48, 0x67, 0x75, 0x75, 0x41, 0x64, 0x5a, 0x76, 0x6c, 0x34, 0x62, 0x67, 0x4f, 0x62, 0x5a, 0x6a, 0x7a, 0x77, 0x37, 0x4c, 0x52, 0x55, 0x73, 0x58, 0x69, 0x55, 0x4b, 0x73, 0x70, 0x74, 0x63, 0x46, 0x7a, 0x59, 0x4e, 0x72, 0x66, 0x79, 0x64, 0x79, 0x49, 0x6e, 0x30, 0x32, 0x37, 0x63, 0x41, 0x56, 0x72, 0x6a, 0x46, 0x44, 0x31, 0x55, 0x54, 0x46, 0x6d, 0x75, 0x35, 0x49, 0x42, 0x4f, 0x72, 0x32, 0x68, 0x43, 0x67, 0x6d, 0x33, 0x39, 0x36, 0x64, 0x54, 0x76, 0x41, 0x46, 0x41, 0x45, 0x38, 0x4c, 0x77, 0x4b, 0x49, 0x38, 0x44, 0x6e, 0x68, 0x6d, 0x78, 0x72, 0x62, 0x42, 0x49, 0x36, 0x58, 0x41, 0x51, 0x4b, 0x6d, 0x35, 0x51, 0x50, 0x73, 0x79, 0x71, 0x44, 0x49, 0x70, 0x6d, 0x30, 0x51, 0x75, 0x6b, 0x5f, 0x4a, 0x77, 0x58, 0x4b, 0x34, 0x48, 0x54, 0x30, 0x78, 0x29, 0x66, 0x72, 0x58, 0x64, 0x32, 0x66, 0x56, 0x73, 0x66, 0x6b, 0x4a, 0x47, 0x35, 0x49, 0x46, 0x71, 0x6c, 0x70, 0x64, 0x61, 0x6b, 0x6e, 0x39, 0x52, 0x56, 0x6c, 0x42, 0x68, 0x79, 0x48, 0x43, 0x61, 0x61, 0x5f, 0x4d, 0x67, 0x37, 0x54, 0x74, 0x47, 0x78, 0x68, 0x4b, 0x6a, 0x64, 0x59, 0x4b, 0x35, 0x4e, 0x4d, 0x78, 0x3e, 0x74, 0x4d, 0x66, 0x76, 0x45, 0x42, 0x46, 0x64, 0x6e, 0x6c, 0x38, 0x66, 0x79, 0x74, 0x39, 0x72, 0x61, 0x4a, 0x6b, 0x46, 0x55, 0x58, 0x46, 0x4f, 0x58, 0x68, 0x65, 0x73, 0x76, 0x71, 0x67, 0x71, 0x52, 0x7a, 0x33, 0x4f, 0x6c, 0x35, 0x75, 0x32, 0x47, 0x4c, 0x38, 0x39, 0x6d, 0x6e, 0x4e, 0x33, 0x4b, 0x43, 0x53, 0x66, 0x59, 0x6b, 0x46, 0x63, 0x6b, 0x56, 0x4c, 0x63, 0x31, 0x4e, 0x6d, 0x46, 0x79, 0x39, 0x4c, 0x74, 0x4f, 0x4d, 0x6d, 0x4d, 0x6e, 0x42, 0x59, 0x45, 0xa6, 0x61, 0x42, 0x6a, 0x59, 0x61, 0x67, 0x58, 0x71, 0x6f, 0x41, 0x52, 0x4b, 0x36, 0x62, 0x56, 0x50, 0x68, 0x71, 0x57, 0x4c, 0x5a, 0x31, 0x4d, 0x49, 0x32, 0x65, 0x70, 0x6e, 0x76, 0x70, 0x4e, 0xa1, 0x61, 0x55, 0x62, 0x4e, 0x72, 0x65, 0x72, 0x6f, 0x68, 0x45, 0x30, 0x65, 0x63, 0x76, 0x55, 0x6d, 0x43, 0x64, 0x50, 0x62, 0x68, 0x77, 0x66, 0x37, 0x34, 0x74, 0x37, 0x68, 0x72, 0x62, 0x54, 0x4a, 0x63, 0x33, 0x4d, 0x6b, 0x62, 0x41, 0x6b, 0x78, 0x1c, 0x4c, 0x58, 0x79, 0x36, 0x34, 0x4a, 0x4b, 0x79, 0x42, 0x4b, 0x46, 0x5f, 0x31, 0x49, 0x5a, 0x45, 0x5f, 0x41, 0x38, 0x4c, 0x30, 0x46, 0x6b, 0x54, 0x73, 0x59, 0x6c, 0x33, 0x66, 0x68, 0x54, 0x33, 0x6c, 0x53, 0x78, 0x78, 0x1e, 0x66, 0x71, 0x69, 0x33, 0x52, 0x6d, 0x70, 0x35, 0x73, 0x7a, 0x32, 0x41, 0x58, 0x34, 0x32, 0x43, 0x4b, 0x64, 0x32, 0x67, 0x6b, 0x68, 0x31, 0x4a, 0x4b, 0x5f, 0x36, 0x76, 0x4f, 0x36, 0x61, 0x55, 0xa8, 0x61, 0x31, 0x67, 0x76, 0x6c, 0x6e, 0x43, 0x4c, 0x78, 0x30, 0x63, 0x55, 0x37, 0x4b, 0x63, 0x76, 0x37, 0x4c, 0x66, 0x71, 0x50, 0x61, 0x31, 0x74, 0x38, 0x69, 0x63, 0x43, 0x53, 0x6a, 0x67, 0x31, 0x7a, 0x77, 0x38, 0x63, 0x55, 0x6f, 0x43, 0x62, 0x79, 0x4d, 0x61, 0x5f, 0x64, 0x59, 0x57, 0x4b, 0x31, 0x62, 0x72, 0x62, 0x64, 0x66, 0x75, 0x46, 0x62, 0x63, 0x47, 0x50, 0x77, 0x62, 0x6f, 0x63, 0x61, 0x4f, 0x64, 0x79, 0x65, 0x59, 0x68, 0x78, 0x2d, 0x7a, 0x6c, 0x68, 0x4d, 0x6b, 0x53, 0x53, 0x66, 0x30, 0x4f, 0x57, 0x77, 0x76, 0x35, 0x53, 0x36, 0x64, 0x35, 0x76, 0x71, 0x4a, 0x5f, 0x32, 0x53, 0x68, 0x4c, 0x4d, 0x34, 0x49, 0x6f, 0x48, 0x30, 0x66, 0x78, 0x68, 0x32, 0x4c, 0x77, 0x53, 0x4f, 0x4a, 0x39, 0x6c, 0x72, 0x38, 0xa4, 0x61, 0x6e, 0x66, 0x71, 0x5a, 0x37, 0x5a, 0x5a, 0x72, 0x62, 0x69, 0x67, 0x6d, 0x36, 0x62, 0x6b, 0x45, 0x75, 0x6b, 0x71, 0x31, 0x4d, 0x59, 0x55, 0x46, 0x56, 0x62, 0x4c, 0x64, 0x65, 0x58, 0x48, 0x75, 0x76, 0x54, 0x61, 0x41, 0x6d, 0x31, 0x52, 0x6a, 0x50, 0x67, 0x35, 0x39, 0x70, 0x53, 0x55, 0x6f, 0x34, 0x59, 0x78, 0x37, 0x4d, 0x76, 0x4c, 0x61, 0x6c, 0x78, 0x32, 0x51, 0x35, 0x51, 0x70, 0x61, 0x47, 0x61, 0x56, 0x58, 0x5f, 0x6a, 0x30, 0x5a, 0x39, 0x59, 0x4e, 0x6d, 0x39, 0x70, 0x31, 0x6d, 0x52, 0x63, 0x57, 0x45, 0x53, 0x61, 0x4d, 0x57, 0x4e, 0x4b, 0x6d, 0x68, 0x78, 0x63, 0x31, 0x45, 0x41, 0x6a, 0x43, 0x48, 0x61, 0x4c, 0x59, 0x35, 0x66, 0x71, 0x42, 0xa4, 0x61, 0x46, 0x66, 0x38, 0x48, 0x79, 0x64, 0x46, 0x53, 0x61, 0x48, 0x69, 0x57, 0x6c, 0x6c, 0x6f, 0x51, 0x74, 0x46, 0x6f, 0x61, 0x67, 0x45, 0x71, 0x36, 0x4f, 0x48, 0x30, 0x47, 0x6a, 0x51, 0x6d, 0x45, 0x4c, 0x54, 0x44, 0x63, 0x50, 0x30, 0x65, 0x65, 0x6b, 0x70, 0x59, 0x5a, 0x73, 0x6e, 0x39, 0x6b, 0x48, 0x58, 0x52, 0x5a, 0x54, 0x64, 0x6b, 0x65, 0x42, 0x39, 0x41, 0x49, 0x76, 0x79, 0x6c, 0x56, 0x68, 0x62, 0x64, 0x76, 0x4c, 0x30, 0x73, 0x51, 0x43, 0x35, 0x48, 0x51, 0x72, 0x68, 0x58, 0x41, 0x74, 0x6a, 0x4c, 0x78, 0x60, 0x4a, 0x4d, 0x72, 0x31, 0x6a, 0x67, 0x68, 0x4e, 0x43, 0x61, 0x6e, 0x46, 0x69, 0x35, 0x5a, 0x73, 0x76, 0x45, 0x43, 0x6e, 0x6a, 0x71, 0x51, 0x69, 0x44, 0x58, 0x5a, 0x36, 0x37, 0x59, 0x4b, 0x32, 0x46, 0x58, 0x38, 0x74, 0x7a, 0x61, 0x54, 0x35, 0x72, 0x49, 0x36, 0x41, 0x6a, 0x54, 0x4e, 0x5f, 0x35, 0x6f, 0x65, 0x4b, 0x48, 0x53, 0x68, 0x4b, 0x45, 0x30, 0x74, 0x45, 0x5f, 0x48, 0x38, 0x6d, 0x51, 0x4f, 0x59, 0x47, 0x68, 0x46, 0x74, 0x41, 0x39, 0x78, 0x49, 0x71, 0x61, 0x63, 0x73, 0x6c, 0x65, 0x54, 0x31, 0x54, 0x54, 0x6c, 0x4c, 0x78, 0x37, 0x49, 0x37, 0x58, 0x4f, 0x37, 0x56, 0x48, 0x6d, 0x66, 0x34, 0x49, 0x36, 0x4f, 0x6d, 0x33, 0x46, 0x70, 0x6c, 0x50, 0x6e, 0x37, 0x78, 0x18, 0x69, 0x47, 0x4c, 0x37, 0x6f, 0x67, 0x43, 0x53, 0x62, 0x62, 0x78, 0x54, 0x6a, 0x64, 0x6c, 0x30, 0x32, 0x70, 0x59, 0x51, 0x49, 0x58, 0x32, 0x49, 0x73, 0x4b, 0x47, 0x4e, 0x43, 0x39, 0x44, 0x71, 0x38, 0x64, 0x49, 0x78, 0x30, 0x61, 0x66, 0x35, 0x49, 0x67, 0x68, 0x51, 0xa5, 0x61, 0x31, 0x61, 0x46, 0x64, 0x41, 0x78, 0x36, 0x35, 0xa1, 0x61, 0x5f, 0x69, 0x58, 0x6a, 0x45, 0x56, 0x56, 0x41, 0x35, 0x73, 0x6f, 0x65, 0x6d, 0x71, 0x67, 0x66, 0x50, 0x69, 0x41, 0x4f, 0x75, 0x67, 0x58, 0x6b, 0x5a, 0x35, 0x6b, 0x64, 0x76, 0x4d, 0x45, 0x68, 0x63, 0x6f, 0x65, 0x70, 0x62, 0x77, 0x54, 0x64, 0x57, 0x77, 0x57, 0x72, 0x75, 0x75, 0x45, 0x59, 0x52, 0x45, 0x6a, 0x72, 0x30, 0x68, 0x32, 0x6c, 0x6f, 0x38, 0x63, 0x32, 0x5a, 0x4b, 0x53, 0x74, 0x57, 0x68, 0x78, 0x30, 0x72, 0x35, 0x34, 0x49, 0x34, 0x76, 0x41, 0x4a, 0x71, 0x6b, 0x6e, 0x70, 0x63, 0x52, 0x35, 0x4d, 0x6a, 0x31, 0x4d, 0x59, 0x4e, 0x58, 0x79, 0x68, 0x44, 0x76, 0x71, 0x5a, 0x76, 0x63, 0x58, 0x79, 0x35, 0x47, 0x56, 0x50, 0x4e, 0x76, 0x6c, 0x4a, 0x52, 0x4d, 0x32, 0x31, 0x51, 0x32, 0x4e, 0x4e, 0x78, 0x20, 0x77, 0x50, 0x38, 0x75, 0x39, 0x32, 0x74, 0x33, 0x4f, 0x71, 0x79, 0x63, 0x52, 0x58, 0x41, 0x32, 0x7a, 0x38, 0x4b, 0x59, 0x5a, 0x41, 0x59, 0x33, 0x51, 0x47, 0x70, 0x57, 0x41, 0x4c, 0x6e, 0x7a, 0x78, 0x4f, 0x41, 0x4e, 0x52, 0x77, 0x42, 0x49, 0x70, 0x70, 0x32, 0x76, 0x4f, 0x6d, 0x55, 0x7a, 0x4d, 0x76, 0x51, 0x4b, 0x4f, 0x70, 0x71, 0x46, 0x52, 0x31, 0x4f, 0x30, 0x42, 0x31, 0x47, 0x69, 0x78, 0x42, 0x61, 0x58, 0x62, 0x56, 0x5a, 0x47, 0x47, 0x70, 0x5a, 0x35, 0x77, 0x65, 0x4a, 0x4b, 0x6a, 0x48, 0x63, 0x71, 0x78, 0x42, 0x4e, 0x5f, 0x71, 0x6d, 0x34, 0x79, 0x45, 0x63, 0x72, 0x50, 0x41, 0x4e, 0x50, 0x54, 0x37, 0x4a, 0x63, 0x5f, 0x31, 0x56, 0x67, 0x64, 0x67, 0x39, 0x6c, 0x79, 0x74, 0x78, 0x21, 0x74, 0x49, 0x6e, 0x45, 0x31, 0x44, 0x4d, 0x37, 0x48, 0x4f, 0x41, 0x50, 0x52, 0x39, 0x4b, 0x4f, 0x65, 0x62, 0x37, 0x4b, 0x43, 0x77, 0x58, 0x30, 0x42, 0x31, 0x68, 0x53, 0x62, 0x33, 0x45, 0x75, 0x70, 0x78, 0x2e, 0x73, 0x78, 0x34, 0x68, 0x39, 0x63, 0x73, 0x75, 0x58, 0x4f, 0x78, 0x42, 0x49, 0x61, 0x39, 0x31, 0x70, 0x4e, 0x78, 0x50, 0x66, 0x70, 0x72, 0x65, 0x55, 0x7a, 0x72, 0x53, 0x46, 0x4e, 0x51, 0x50, 0x75, 0x4a, 0x70, 0x67, 0x7a, 0x6c, 0x54, 0x45, 0x46, 0x39, 0x4c, 0x4e, 0x58, 0x54, 0x69, 0x48, 0x74, 0x79, 0x58, 0x34, 0x44, 0x62, 0x57, 0x70, 0x78, 0x52, 0x72, 0x6e, 0x77, 0x46, 0x45, 0x4e, 0x6d, 0x51, 0x4a, 0x47, 0x5a, 0x68, 0x49, 0x49, 0x64, 0x51, 0x45, 0x72, 0x6f, 0x4f, 0x4b, 0x70, 0x6e, 0x65, 0x37, 0x58, 0x79, 0x74, 0x4c, 0x4d, 0x42, 0x70, 0x52, 0x4a, 0x52, 0x68, 0x7a, 0x30, 0x42, 0x6a, 0x61, 0x64, 0x76, 0x51, 0x61, 0x52, 0x64, 0x56, 0x72, 0x47, 0x4e, 0x53, 0x70, 0x6b, 0x6a, 0x69, 0x6d, 0x48, 0x66, 0x39, 0x47, 0x4e, 0x49, 0x4a, 0x74, 0x4f, 0x38, 0x78, 0x73, 0x4b, 0x31, 0x39, 0x36, 0x4b, 0x38, 0x41, 0x71, 0x67, 0x4b, 0x37, 0x5a, 0x65, 0x64, 0x54, 0x69, 0x62, 0x41, 0xa3, 0x61, 0x42, 0x67, 0x5a, 0x77, 0x41, 0x31, 0x6e, 0x52, 0x70, 0x67, 0x64, 0x67, 0x34, 0x47, 0x35, 0x70, 0x53, 0x6a, 0x73, 0x59, 0x6d, 0x73, 0x6c, 0x74, 0x66, 0x56, 0x6f, 0x53, 0x66, 0x34, 0x76, 0x4b, 0x6d, 0x6d, 0x70, 0x6a, 0x70, 0x37, 0x4a, 0x48, 0x59, 0x58, 0x5a, 0x69, 0x54, 0x5a, 0x77, 0x43, 0x35, 0x65, 0x49, 0x58, 0x43, 0x62, 0x31, 0x74, 0x47, 0x63, 0x34, 0x63, 0x74, 0x5a, 0x4e, 0x6f, 0x45, 0x4e, 0x64, 0x79, 0x39, 0x43, 0xa6, 0x61, 0x57, 0x66, 0x6e, 0x30, 0x59, 0x5a, 0x54, 0x37, 0x63, 0x65, 0x30, 0x64, 0x64, 0x51, 0x63, 0x47, 0x44, 0x61, 0x47, 0x65, 0x53, 0x64, 0x4c, 0x35, 0x64, 0x63, 0x62, 0x61, 0x6c, 0xa1, 0x61, 0x72, 0x68, 0x79, 0x50, 0x77, 0x63, 0x7a, 0x74, 0x71, 0x6f, 0x65, 0x67, 0x42, 0x54, 0x64, 0x42, 0xa1, 0x61, 0x73, 0x61, 0x41, 0x63, 0x42, 0x6d, 0x45, 0xa1, 0x61, 0x32, 0x6d, 0x4a, 0x44, 0x56, 0x4d, 0x6b, 0x59, 0x61, 0x51, 0x66, 0x74, 0x32, 0x49, 0x4b, 0x70, 0x73, 0x66, 0x58, 0x46, 0x39, 0x6d, 0x6c, 0x6d, 0x4d, 0x42, 0x77, 0x63, 0x52, 0x54, 0x41, 0x35, 0xa5, 0x61, 0x54, 0x63, 0x72, 0x4a, 0x7a, 0x64, 0x56, 0x63, 0x39, 0x45, 0x64, 0x6c, 0x47, 0x5a, 0x32, 0x61, 0x52, 0x65, 0x67, 0x44, 0x64, 0x54, 0x76, 0x61, 0x48, 0x6b, 0x7a, 0x6c, 0x66, 0x37, 0x71, 0x54, 0x38, 0x71, 0x79, 0x38, 0x6a, 0x62, 0x74, 0x54, 0x6d, 0x58, 0x59, 0x68, 0x77, 0x4f, 0x62, 0x72, 0x38, 0x57, 0x64, 0x65, 0x67, 0x5f, 0x6e, 0x37, 0x61, 0x47, 0x4e, 0x72, 0x53, 0x72, 0x30, 0x68, 0x44, 0x7a, 0x6a, 0x74, 0x61, 0xa3, 0x61, 0x36, 0x63, 0x79, 0x30, 0x35, 0x64, 0x4d, 0x33, 0x6b, 0x66, 0x6b, 0x41, 0x62, 0x6d, 0x44, 0x52, 0x6c, 0x4f, 0x7a, 0x79, 0x4d, 0x5f, 0x66, 0x42, 0x41, 0x4c, 0x55, 0x53, 0x77, 0x66, 0x57, 0x43, 0x44, 0x6e, 0x6e, 0x42, 0x63, 0x73, 0x39, 0x61, 0x78, 0x37, 0x65, 0x6b, 0x65, 0x31, 0x7a, 0x6a, 0x45, 0x65, 0x59, 0x6c, 0x66, 0x4f, 0x58, 0x71, 0x72, 0x4c, 0x5a, 0x41, 0x4a, 0x78, 0x44, 0x64, 0x78, 0x50, 0x33, 0x30, 0x42, 0x57, 0x4b, 0x67, 0x59, 0x46, 0x7a, 0x36, 0x6c, 0x41, 0x58, 0x6a, 0x5f, 0x42, 0x4b, 0x7a, 0x73, 0x36, 0x62, 0x44, 0x6a, 0x47, 0x6f, 0x6b, 0x73, 0x77, 0x79, 0x65, 0x6b, 0x71, 0x50, 0x57, 0x61, 0x79, 0x41, 0x49, 0x59, 0x58, 0x4f, 0x43, 0x68, 0x41, 0x70, 0x37, 0x44, 0x33, 0x45, 0x78, 0x31, 0x44, 0x51, 0x77, 0x6f, 0x42, 0x45, 0x36, 0x68, 0x38, 0x69, 0x4c, 0x4d, 0x44, 0x4a, 0x6e, 0x62, 0x56, 0x45, 0x43, 0x78, 0x54, 0x52, 0x66, 0x70, 0x4e, 0x4d, 0x55, 0x43, 0x45, 0x4c, 0x44, 0x52, 0x70, 0x4c, 0x73, 0x6d, 0x5f, 0x44, 0x57, 0x74, 0x37, 0x71, 0x64, 0x6c, 0x58, 0x61, 0x39, 0x42, 0x71, 0x73, 0x32, 0x6e, 0x59, 0x67, 0x32, 0x63, 0x4c, 0x4a, 0x43, 0x4b, 0x73, 0x49, 0x68, 0x54, 0x5a, 0x30, 0x75, 0x39, 0x4f, 0x78, 0x24, 0x75, 0x39, 0x39, 0x64, 0x52, 0x39, 0x4b, 0x73, 0x6c, 0x79, 0x5a, 0x33, 0x6d, 0x58, 0x47, 0x55, 0x49, 0x34, 0x34, 0x34, 0x47, 0x44, 0x5a, 0x4f, 0x39, 0x71, 0x4c, 0x4b, 0x41, 0x79, 0x62, 0x6c, 0x74, 0x68, 0x72, 0x30, 0x78, 0x1f, 0x4a, 0x5f, 0x6b, 0x31, 0x53, 0x32, 0x6e, 0x67, 0x4c, 0x72, 0x61, 0x51, 0x4e, 0x53, 0x51, 0x58, 0x7a, 0x6c, 0x48, 0x56, 0x6f, 0x7a, 0x57, 0x43, 0x6e, 0x76, 0x63, 0x36, 0x46, 0x39, 0x72, 0xa3, 0x61, 0x64, 0x64, 0x50, 0x6c, 0x59, 0x52, 0x63, 0x34, 0x65, 0x36, 0x62, 0x4b, 0x57, 0x67, 0x56, 0x5f, 0x4b, 0x47, 0x64, 0x43, 0x48, 0x67, 0x4c, 0x64, 0x35, 0x4a, 0x4b, 0x44, 0x34, 0x78, 0x21, 0x66, 0x54, 0x7a, 0x67, 0x74, 0x53, 0x56, 0x30, 0x74, 0x47, 0x71, 0x4f, 0x72, 0x47, 0x66, 0x43, 0x36, 0x46, 0x4b, 0x71, 0x4d, 0x57, 0x7a, 0x69, 0x75, 0x6f, 0x33, 0x32, 0x44, 0x78, 0x5a, 0x78, 0x51, 0x78, 0x4a, 0x4e, 0x43, 0x47, 0x43, 0x34, 0x70, 0x36, 0x65, 0x63, 0x50, 0x59, 0x45, 0x4f, 0x4c, 0x73, 0x6f, 0x61, 0x41, 0x55, 0x45, 0x61, 0x38, 0x7a, 0x30, 0x4b, 0x76, 0x49, 0x33, 0x6f, 0x47, 0x57, 0x48, 0x34, 0x41, 0x5f, 0x53, 0x6f, 0x34, 0x4b, 0x4e, 0x67, 0x73, 0x77, 0x57, 0x35, 0x76, 0x44, 0x36, 0x62, 0x71, 0x58, 0x6b, 0x6b, 0x42, 0x39, 0x73, 0x75, 0x56, 0x4c, 0x5a, 0x73, 0x49, 0x63, 0x6e, 0x64, 0x68, 0x56, 0x53, 0x51, 0x6b, 0x76, 0x62, 0x71, 0x51, 0x68, 0x6b, 0x33, 0x7a, 0x4f, 0x76, 0x75, 0x7a, 0x70, 0x78, 0x1f, 0x67, 0x35, 0x77, 0x77, 0x58, 0x42, 0x6f, 0x36, 0x48, 0x49, 0x36, 0x35, 0x76, 0x37, 0x73, 0x4f, 0x6b, 0x56, 0x38, 0x4c, 0x51, 0x4a, 0x4d, 0x62, 0x4c, 0x49, 0x4a, 0x70, 0x38, 0x39, 0x44, 0x78, 0x23, 0x4f, 0x74, 0x52, 0x5f, 0x48, 0x65, 0x52, 0x51, 0x54, 0x62, 0x34, 0x6c, 0x6c, 0x51, 0x5a, 0x61, 0x76, 0x4c, 0x75, 0x79, 0x63, 0x53, 0x64, 0x62, 0x33, 0x65, 0x39, 0x4b, 0x6a, 0x4d, 0x30, 0x72, 0x77, 0x71, 0x59, 0x78, 0x55, 0x50, 0x76, 0x70, 0x46, 0x53, 0x73, 0x5a, 0x6b, 0x79, 0x65, 0x55, 0x76, 0x5f, 0x6b, 0x7a, 0x32, 0x55, 0x68, 0x5f, 0x43, 0x4d, 0x39, 0x73, 0x41, 0x4f, 0x68, 0x69, 0x69, 0x6c, 0x30, 0x4d, 0x6d, 0x61, 0x76, 0x4e, 0x6f, 0x37, 0x5f, 0x73, 0x58, 0x32, 0x6d, 0x33, 0x69, 0x4b, 0x67, 0x34, 0x31, 0x6f, 0x48, 0x69, 0x46, 0x68, 0x6b, 0x59, 0x59, 0x54, 0x46, 0x63, 0x6c, 0x75, 0x77, 0x5f, 0x4c, 0x41, 0x77, 0x72, 0x31, 0x44, 0x53, 0x5f, 0x6a, 0x31, 0x53, 0x4a, 0x64, 0x49, 0x62, 0x52, 0x56, 0x4b, 0x52, 0x74, 0x33, 0x4b, 0x66, 0x75, 0x4d, 0x56, 0x4d, 0x64, 0x61, 0xa7, 0x61, 0x32, 0x6b, 0x70, 0x76, 0x36, 0x78, 0x73, 0x47, 0x54, 0x6c, 0x4e, 0x71, 0x75, 0x61, 0x64, 0x65, 0x47, 0x33, 0x54, 0x50, 0x78, 0x63, 0x42, 0x56, 0x41, 0x64, 0x51, 0x51, 0x39, 0x51, 0x62, 0x48, 0x6a, 0x6e, 0x32, 0x34, 0x66, 0x37, 0x36, 0x50, 0x50, 0x6f, 0x45, 0x70, 0x52, 0x67, 0x58, 0x64, 0x61, 0x4b, 0x69, 0x54, 0x67, 0x4e, 0x46, 0x50, 0x6d, 0x76, 0x79, 0x57, 0x69, 0x74, 0x78, 0x70, 0x72, 0x72, 0x34, 0x61, 0x69, 0x41, 0x69, 0x46, 0x34, 0x4d, 0x42, 0x32, 0x72, 0x75, 0x54, 0x55, 0x63, 0x73, 0x6f, 0x31, 0x68, 0x63, 0x76, 0x7a, 0x6d, 0x6c, 0x5a, 0x6c, 0x7a, 0x61, 0x71, 0x78, 0x4c, 0x52, 0x62, 0x31, 0x70, 0x54, 0x46, 0x4c, 0x4b, 0x43, 0x78, 0x74, 0x4c, 0x6d, 0x32, 0x77, 0x4c, 0x6d, 0x4b, 0x73, 0x51, 0x47, 0x42, 0x5f, 0x50, 0x4d, 0x33, 0x43, 0x5a, 0x50, 0x47, 0x6a, 0x55, 0x44, 0x36, 0x4d, 0x75, 0x39, 0x48, 0x4f, 0x42, 0x7a, 0x63, 0x73, 0x65, 0x69, 0x5f, 0x30, 0x42, 0x4e, 0x5f, 0x4d, 0x55, 0x4f, 0x33, 0x6e, 0x45, 0x6d, 0x66, 0x70, 0x59, 0x5a, 0x4b, 0x56, 0x68, 0x57, 0x64, 0x49, 0x77, 0x49, 0x70, 0x73, 0x38, 0x70, 0x48, 0x4b, 0x45, 0x6a, 0x54, 0x47, 0x65, 0x4e, 0x71, 0x56, 0x53, 0x49, 0x49, 0x38, 0x75, 0x45, 0x47, 0x70, 0x41, 0x67, 0x4f, 0x6b, 0x78, 0x63, 0x62, 0x7a, 0x31, 0x4d, 0x49, 0x55, 0x6a, 0x59, 0x37, 0x6b, 0x7a, 0x43, 0x68, 0x37, 0x77, 0x6e, 0x34, 0x44, 0x56, 0x42, 0x30, 0x78, 0x2a, 0x41, 0x79, 0x64, 0x4b, 0x6d, 0x54, 0x73, 0x31, 0x4a, 0x65, 0x74, 0x59, 0x4c, 0x58, 0x79, 0x39, 0x78, 0x67, 0x73, 0x6e, 0x42, 0x4c, 0x6b, 0x66, 0x51, 0x46, 0x37, 0x53, 0x6d, 0x7a, 0x6e, 0x51, 0x45, 0x53, 0x6c, 0x75, 0x4a, 0x4c, 0x68, 0x33, 0x4b, 0x44, 0x62, 0x36, 0x63, 0x78, 0x24, 0x51, 0x34, 0x6a, 0x4f, 0x37, 0x71, 0x48, 0x4c, 0x6f, 0x53, 0x66, 0x67, 0x45, 0x63, 0x68, 0x74, 0x51, 0x47, 0x69, 0x30, 0x74, 0x41, 0x39, 0x6a, 0x77, 0x31, 0x4a, 0x73, 0x45, 0x38, 0x59, 0x31, 0x44, 0x6a, 0x4a, 0x61, 0x78, 0x1b, 0x59, 0x4d, 0x70, 0x56, 0x41, 0x36, 0x46, 0x58, 0x78, 0x74, 0x4d, 0x6a, 0x6a, 0x64, 0x58, 0x71, 0x6c, 0x6a, 0x42, 0x66, 0x5f, 0x36, 0x6a, 0x56, 0x71, 0x55, 0x35, 0x78, 0x4e, 0x76, 0x75, 0x73, 0x32, 0x50, 0x50, 0x54, 0x71, 0x59, 0x52, 0x46, 0x52, 0x7a, 0x70, 0x62, 0x53, 0x36, 0x69, 0x52, 0x4b, 0x59, 0x71, 0x7a, 0x70, 0x38, 0x59, 0x5a, 0x6c, 0x38, 0x77, 0x65, 0x70, 0x59, 0x7a, 0x62, 0x68, 0x5a, 0x6f, 0x6a, 0x4d, 0x49, 0x34, 0x71, 0x73, 0x4b, 0x4e, 0x6f, 0x75, 0x72, 0x32, 0x44, 0x53, 0x71, 0x34, 0x45, 0x6f, 0x6b, 0x33, 0x41, 0x37, 0x4f, 0x34, 0x6c, 0x52, 0x58, 0x46, 0x78, 0x45, 0x37, 0x41, 0x51, 0x32, 0x79, 0x63, 0x63, 0x51, 0x54, 0x4b, 0x78, 0x22, 0x6c, 0x74, 0x44, 0x64, 0x48, 0x54, 0x77, 0x77, 0x4f, 0x31, 0x44, 0x6a, 0x57, 0x59, 0x66, 0x4e, 0x33, 0x63, 0x48, 0x6e, 0x74, 0x6e, 0x69, 0x42, 0x74, 0x4a, 0x45, 0x70, 0x70, 0x59, 0x74, 0x51, 0x49, 0x62, 0x78, 0x4b, 0x55, 0x6b, 0x46, 0x71, 0x6a, 0x67, 0x44, 0x63, 0x71, 0x61, 0x64, 0x73, 0x61, 0x68, 0x37, 0x6e, 0x63, 0x7a, 0x38, 0x75, 0x4c, 0x75, 0x48, 0x39, 0x52, 0x71, 0x30, 0x73, 0x66, 0x35, 0x6b, 0x6a, 0x43, 0x77, 0x6b, 0x6e, 0x52, 0x46, 0x4f, 0x30, 0x31, 0x75, 0x6f, 0x65, 0x53, 0x74, 0x34, 0x4a, 0x76, 0x77, 0x76, 0x6e, 0x56, 0x72, 0x38, 0x78, 0x36, 0x68, 0x66, 0x4b, 0x6d, 0x78, 0x50, 0x77, 0x69, 0x76, 0x35, 0x38, 0x61, 0x41, 0x35, 0x6f, 0x37, 0x34, 0x41, 0x6c, 0x77, 0x4b, 0x4c, 0x6f, 0x44, 0x73, 0x76, 0x50, 0x37, 0x67, 0x6a, 0x4f, 0x78, 0x6a, 0x4e, 0x69, 0x65, 0x6b, 0x55, 0x59, 0x4a, 0x37, 0x47, 0x76, 0x47, 0x69, 0x37, 0x4a, 0x52, 0x58, 0x6f, 0x69, 0x36, 0x75, 0x7a, 0x48, 0x46, 0x4d, 0x39, 0x59, 0x65, 0x58, 0x44, 0x4f, 0x68, 0x70, 0x52, 0x39, 0x79, 0x39, 0x44, 0x41, 0x66, 0x39, 0x4c, 0x4c, 0x31, 0x67, 0x72, 0x49, 0x4e, 0x6f, 0x42, 0x72, 0x7a, 0x67, 0x45, 0x30, 0x58, 0x64, 0x73, 0x38, 0x53, 0x58, 0x35, 0x34, 0x72, 0x37, 0x45, 0x4c, 0x61, 0x42, 0x32, 0x4a, 0x45, 0x35, 0x75, 0x6c, 0x70, 0x34, 0x74, 0x37, 0x67, 0x78, 0x6e, 0x30, 0x66, 0x31, 0x68, 0x6e, 0x51, 0x5a, 0x4c, 0x53, 0x4a, 0x4c, 0x75, 0x7a, 0x71, 0x56, 0x36, 0x47, 0x76, 0x73, 0x4c, 0x55, 0x35, 0x59, 0x75, 0x48, 0x69, 0x70, 0x54, 0x6f, 0x64, 0x34, 0x48, 0x44, 0x69, 0x4c, 0x78, 0x20, 0x59, 0x4d, 0x39, 0x50, 0x51, 0x61, 0x5a, 0x45, 0x62, 0x41, 0x6e, 0x4a, 0x59, 0x62, 0x74, 0x4d, 0x54, 0x33, 0x49, 0x68, 0x70, 0x6e, 0x47, 0x62, 0x78, 0x30, 0x44, 0x65, 0x43, 0x36, 0x39, 0x73, 0x6f, 0x64, 0x78, 0x58, 0x4d, 0x4b, 0x4d, 0x37, 0x41, 0x37, 0x75, 0x70, 0x78, 0x56, 0x63, 0x7a, 0x78, 0x32, 0x34, 0x6f, 0x62, 0x5f, 0x44, 0x79, 0x69, 0x67, 0x61, 0x48, 0x48, 0x37, 0x68, 0x4e, 0x75, 0x78, 0x63, 0x63, 0x6a, 0x42, 0x66, 0x4c, 0x38, 0x52, 0x5f, 0x42, 0x74, 0x65, 0x76, 0x59, 0x46, 0x7a, 0x5a, 0x31, 0x38, 0x6e, 0x69, 0x42, 0x43, 0x4d, 0x30, 0x4a, 0x5a, 0x64, 0x61, 0x61, 0x32, 0x71, 0x35, 0x33 }; + char buf[256] = {}; + size_t offs = 0; + while (offs < sizeof(expectedCborData)) { + size_t n = std::min(sizeof(buf), sizeof(expectedCborData) - offs); + assertEqual(ledger_read(stream, buf, n, nullptr /* reserved */), (int)n); + assertEqual(std::memcmp(buf, expectedCborData + offs, n), 0); + offs += n; + } +} + +#endif // Wiring_Ledger diff --git a/user/tests/integration/communication/ledger/ledger.spec.js b/user/tests/integration/communication/ledger/ledger.spec.js new file mode 100644 index 0000000000..559bec40bb --- /dev/null +++ b/user/tests/integration/communication/ledger/ledger.spec.js @@ -0,0 +1,64 @@ +suite('Ledger') + +platform('gen3'); + +const Particle = require('particle-api-js'); + +const DEVICE_TO_CLOUD_LEDGER = 'test-device-to-cloud'; +const CLOUD_TO_DEVICE_LEDGER = 'test-cloud-to-device'; +const ORG_ID = 'particle'; // Set this constant undefined to use the sandbox account + +let api; +let auth; +let device; +let deviceId; +let seed; + +async function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +before(function() { + api = new Particle(); + auth = this.particle.apiClient.token; + device = this.particle.devices[0]; + deviceId = device.id; +}); + +test('01_init_ledgers', async function() { + expect(device.mailBox).to.not.be.empty; + seed = Number.parseInt(device.mailBox[0].d); +}); + +test('02_sync_device_to_cloud', async function() { + await delay(1000); + const { body: { instance } } = await api.getLedgerInstance({ ledgerName: DEVICE_TO_CLOUD_LEDGER, scopeValue: deviceId, org: ORG_ID, auth }); + expect(instance.data).to.deep.equal({ a: seed + 1 }); +}); + +test('03_update_cloud_to_device', async function() { + const data = { b: seed + 2 }; + await api.setLedgerInstance({ ledgerName: CLOUD_TO_DEVICE_LEDGER, instance: { data }, scopeValue: deviceId, org: ORG_ID, auth }); +}); + +test('04_validate_cloud_to_device_sync', async function() { + // See ledger.cpp +}); + +test('05_sync_device_to_cloud_max_size', async function() { + await delay(1000); + const { body: { instance } } = await api.getLedgerInstance({ ledgerName: DEVICE_TO_CLOUD_LEDGER, scopeValue: deviceId, org: ORG_ID, auth }); + const expectedJsonData = '{"3":"5Q50PPeWFBs7A7JdhYun6jd95AyY14yi89VSuwB5Zqx1Q4uZPCBC5iuoLlrbAcDqB1bwlc0_iiUVeWglQ74wrWPeTx1lM3ZNPyg3FrniFCD82i6Vlcls","55":"hzs3HKol0kc1FsQknb8at6Vda5vxG65Sh8xmqqp4biuflPGL_3GmIc0M9Htmk9WJsn0ezPkPElqY_GI9e","X":"ou8","CMsyW9":"_r0tKjaiKKdpwexpcgBtTnQvSYd2vV8j0rFqocP3pTwL92Cw","qixfSDCURWhgSuI7LuASzRPqggPIK3coKcv1ObCTBjGa8xY":"njRKCb2JUi0xSbingqzwoQqhwg8esbDRZAxT4_oSBqb2k2RpgLa2Ziwvhuw","l8WBEdLvb0k3YsfFmaAhjA_WNaQ5fRHkgsX":{"32":"F1a5KDxdV","A":"KRI","VKbg":"jUFti7hmbywuXKu","BE":"8G5_FX","RV":"mEger027QgRLl2voM"},"0Go5q47rR0uqbBcgnJ1R":{"c":"","CEuDg":"apgar28c","rr":"_5XquQw","Njl":{"p":"JS"},"9nEs9r":"kBreODE","s":"N8Rs","V2":"wOeaU2ga","0P":"hR9MQaMVCxx"},"7hhv4k":"RTXJIh16UP2QOzWBXYLYWDKNhxGtabCN3IR1bhScZuU9sAk8v8gRdeSLoFTRG8vdNWl5oUUEH1VSHQOc5RmSObGtM","7hINNqwV":{"X":"kCxOh3eJ","p":"3aRB663r","e":"uvj8x","2EWs":"WSOj","iXZOEEP":"Twm01ijipZwGV","rY":"a74D6Kb"},"hPeUdEqooA12WrNG1SJ0a0xKJcQrK":"zz_QR1f4HQEH0JKvR110I1YROTa4JKM6LW0Ql1vf7BPJrlhM0ZLHI","zI5oqKtu3p4kythQtCyeFfgOLfhmP9MrkVKuxhBMr":"_bouTHlJPlyY_FiTbvpclx4XUP4VDj_ND__E0bmLrxpT1","szfqnLAK47ZeD0YOhCjmn":"wa9SZ98ETz6NTLLDDdqGMiWiXXbEymonpPNMhpi7rUgIDqWEj3RlJGcGixKSueOw0b","gY6EYwhuyaVFQ":"KOaf0nDKlKThHFrTnTLYGJJDOhXznQtAf3rdWbt7o2aYkF35sRCnsKBnysNLS9NHreX9mGKLDMz1ZUSP1","bmI1eWjCV9VMX8ROAmdo":{"g":"l_xN2PdS","d":"SwalXz"},"1e5cMbJvi4fheuIS3Kzcd3L_yeYm1E":"jDCzt1c7yt_k0di8g3ALYbdYTDcxEBWou4h","hONA8j8zmaAwZ":"uSnyxN0pxGjIlV0qUDUUfVlI4fkAKMF_IkCkAP7","wup1SwivSn":"Koeccq33gWv7s2U15mLt9iTJGy__dXE_OJFF6QqiuIDi4YdzQmz","3WF7KK":"oBJcgA1a9XVdRs9hRrsmYwSyEfiMSC8x7rb","e9ghEiFBiemEG3xsiDN5TKx5jrTOhAtK9HTTgl_IWZz":"XYFAcBcwxluQncC4UmWtG2cp5tuUt6Wn6qtylNahQOgMtxiqfE8NqK5RZtMXOs6faINTVtXN","04OeqEwt1eJ6hVGyhXUcRXNYEnxa9uaQ5yYIE2Q0I":"geGZIwSCgL04GPJrMsLfmwXRqaHUhzdnuOI5Aybrd1oe489UpIIxglNZ_VrFslSFllu","nhEIFYmn":"JpgPCGLh6sY2AHKdE1tkGi7A08jn9hECk7u1d15kCet_NngfUSD79CmOlpB4nMgEqIQ4G4c9Ik6o5tRyTXTp1KL8YDnbEDU","9M2fu75PLFohvOctbgV":"QhGSAlagHvxsuCNYRm1ea_x269k0PeCYkQnJT_URrCtOSG40W1G9a6GbhSEf4ywdYXtG4lQvpafadJ2RW0VWDxH9F40oPe","CnG7yPy02":"845ueAasC2_EgvrSRLDVpAz0m0KVA_WQHciYUBDZ2hyh1Sr9mxv7e5oBj15e2VHahRk7AIWcQb8G","mODGZkBQ3JqaDlf7hSK6AlKuugUf":"k5OxhF9uwWRol0w_o3YH4CM0ECzroAVJfliIWB5ncpDsVsPPe0kvNAHLC","Ry7jEpuE5cEKR":"vIh6aGMDKNLLCdmoRTBQfYPznZQrUFt","2kv7SQV3jJDQxrus":"CzjYUwKvUmYjVQL2SxKm9zV_2w2nXCJvYCo98","DzqfyW2":"dNNgc6GXNfas6sDdUWDjqkZ_b3cRu5fjMPPp23aeq","NA9pQzyLD2N9usOx6KN9gVMWEGwsi":"0ye8KkKUKGjCyxnng1T9XW8wGenqSmE93P_zqWmLM13VFUD_0QK7QmUOxKcZtwE9yO8x06oES1nQmJPrzA0nM_9m","FWJs8flTLvgdnSgCiRpZbAKDV16TT":"6VFJ3bxXkRZiOkFHu7_8efUXzg_FM3","APBjvanpH_PM5LqodoLwJlI18z7Fg_5n8k":"VCIj9snR107ymCZS_Q5F_lsvS0PORGexYz8sEgX5jgYpwmkCIljBGoXAaPpKDvxiKJiVa_rKFrX9pS76Bo","GlEo2DnH6P_HZei3cjkPOrzogEwB0KlhsT":"COM0UwFDw7WI58H4ONORm0dyJn47Lx5zgTkzpvU48tPGhLySFyaCVAnv1mv8syOMYt5WQ2_n7QH_","6g92wealsic":"rlxL2gZBxn2tZ1q6S0FJ2C9Ami8t5Fig6g3mFPgFdjw","BypoxRY":"XvTItEoVnYYUXEi9lIadWsso1N6YedARWrutVwlZZpxiR68ryK3GGQySuRll98o00f7ivPbBpjMnexozg5JmfHHtw","agqliBc":"BpLY14rtngZqx_zG3R7Xpa1vAVbgKfy4Q7YTkpbN6BCPhIpQ60NB4nQM1J9S3h8_oVqULrs6YMaRZiy","mxGv3lCy":"9l46UYjaugnBQVBUBhC7MCxkXzaSCSY8eS8GjTKJhn","m":"AFq3Jbh8F_LX__r_rbkTc19YoCmP32nsB","_fi_73618DBX0V9saMKIOEGjV1gEIo8x5D":"NJ28JR4QnHTxiG1adOWp7bigl8M5WemkBWTm9fFyrAVjWTB2fPUy4HIMWmhf1FnnFNGVqxIUqn4JHdNcnH","ngEXJOx8H2gYE":"fWHrzV9Qz64nOjT_r","hHJnBbk7hS_fR4THUDpsdxW0SCVygGYBL":{"g":"eTO4FiYeKtCp","Hec":"36wdP1E","kX":"5TCv"},"XUyqD7JTdX2U6UspgqeYbBu7mz7hayR":"FgfsemPA1zGAkOxL2NwEjvohWuXLAi146ltUAP","kh":"hLhgN_2Jw4uGBifoas1DVCwmxOYeuCjtwvGneBdaW_4g2mHU59mm49","QlfXb8W9K":"THJIbg7TGZ1DrBmNxmpcdKBikQGv3PfEaoXZXk09lCNBRVYCIVJaW0KEMtb0EWzUZcRVxAsFrx4yTkWED3XzSe8bF_gZhFoqQZJ","0DvrVRjUnn8_bVyfzDvb7a":"LRXUYDI4XhrLLA_HuhZXes","y4qrn_6m48cffhVHcmwvULeYCj8E5ACcxeQ49IT":"NFouINkJXj0asqiTno_3g01G6gof10l7Nl943Y7kf8aHNgMI5NN8i2RsPtrigepp8qUwbyRvul","Sy_w3JtyWV":"tlKYPkqnuGRWZOGyMKyVlazgZn46aPtnKKYwT5qbXBFcvRcUF9hVO10YdqQX","TjFWo63TnB87DM3qWueyGGG21FcU0hjA8T":"NgIJjiGnvwizf1qNwC8ctaVkqJxa9cSR5KYKh1DUPAhjfHHR3bXQRRc6fZ1Ix4yYna","qDxwY0WETw_ZAcU":"u_KZyxdx6yxjNYdH5c4ZF1BmrNMhnrpFzHL","pGTDAzc":"VnRPyXID5wrlbWXDr8OpqWGBp8tR6bQdo6tJA20RWPUiSH92oEN","rnME64nDXmNlNxZ8UbCjeRnXLytiBVR":"grKOI4RVbOfpDdgoMinvWuf3tcZhWaO2IYNl2","zyQaxQh":"jRM7gQMmaybBnUF4uXz9ogtVaciyqFcnJAbEaAccBbx","DTRJh5n3":"XMqj1iM6HX4pEMjYhQgBMyvRAp88_IPklU36ztw5ohbEtW1ulTyibtB","_8GLL8KJOYd4mmKBnIqqW":"qFQ82dBFTyyWZwwJWLwX1NIal7b","vq3YYJdakpkpJANvaEgta5GaQW2eCISzrSA4DqOiPSr8jaUBNneaA":"9HpTtQnM074zF1LhOPQv3Mr43judZKqBpmGqgtBdSN9bq81UUFJ0ZpIqrlJuPtxZg","ET4maZ8cI":"UwwafHiliNvNWL_K2f_V4cqYQIujteiOcfGKo4uvEBR0ga0bklqiW_tUO6vlGOzOpHxA3ax_qEQ0i8MyVksEguEpIWVy4eX5e3WEg2AIEh8O","OtA1V8vxNWldU_No8s2VGVGezyY0oiit8gdlT9tiokEID":{"7":"52xZUbo5y","m":"j4m","fm":"QjxAeIGkGJbn0mirl","lQt":"p5li"},"oMXyVf4y_MZ5CqK42LS1TcW":{"0":"","sWCXsa":{"9":"kf8iPOYc"},"kW":"eLoPckKt6"},"mSvzEZQDy8G":{"U":"bLxw8","gjS":"deEN","Cr":"KTzkU"},"5j4":"6O5nnnHQ4T__rbq_CHCekqKDF54","0K22Ruob83aVmNwSmAYFAvUf":{"8":"","I84_Q":"AJtiJGGsvC","Kaq":"7CR5St963b","Q2t":"f0"},"1W2yOXhbVWDGSz":"R8XbjjdN7cJVn8sYnO4lrR6H0Vb2lm60s7SS8GFuPx","sw6o":{"3":"inSXeVmJ3_IvsFiNHK","3Dl":"ihI2","r":"Q8wY3zG4mUES_AG","Md_":"Uw","CKO":"brVp1L","Y5kMl":"RktHdBVedqfhZ","Wr1":"4Jr84"},"tvNSB69lz9LI":"uf9cY3V6xSi6h1iXxK_7Mb","JIfYYfhZhEhJgjWa5lK4tpO11GZO1Wjregpu":{"I":"us1r7w","BVYDT":"qki__oaFF4g","CK0h_dc":"PeR0UIh","cPOP":{"4":"8W9xmD_OPmK"},"wCf":"J_Mk6"},"N_F5ELAI":{"2":"82vJ_MmiFMuj","hsfu":"Ol5nylD"},"w8ERgPVhEh8pkSJ47C":"WdXuzuUWloFxK73V2TgzBi4RhShwmZ8Bjzu_QPxKif2E01Ugc79Y0HqoyU3PfFAaAhvHM7wFUIs0Pi0UT4bDgronOFGps9r_","ZzZi2DAeUKBHdpx6j8iEnNE31jRs1m21l8dCdI05":"iVhTeUGEhzKYEHN6klEgpnQmnrfnFx3WBNi7tPNFdNwilTOMCY","fKNh7F3w8Q":{"L":"5bQD","BFA6Uq":"EYzW8","_uR":"ERF","kQat":"7gzd"},"xOPOOqAENfwh0PiagUwwgrKyvLgU":{"F":"h62","gjX":"TTMPr7DK_5p39W","LKI":"dLdtDY","TUIcH":"tkArmRyIMEm1R","5R":"xb4dTEFshaI"},"Wi":"mjH2ZvQ9SAWFWnAnd4j87uBrOZxRvXzk7jcZE1K2zoOcyO","xe1":"MW2DzubflLLjKZNC4oYqCK9ukr4","GKRlqp3hhIpSin1qNm5uqCBypkTWkICNklRz":"_rm4Vh_JdTcNshzmu_g2bLfYceDEx9EO2b7CCos76qQca5yo7lMzbs_5lHowg","p_CkgfjhrTphWz2jUHVoRJpZoRpbZ813P25TSyQ7l":"TjLVePJ4g0bUeBHKibjHWYC1bn_8M9bFwSHi7brHUmnJTGXJ2J1c3hHKLymwhEboE8tb","vkZshyB2sBSiHd2V1z36tcOGT7":"pGelsP2fXEm2376dHvJtyVjZcTO3g1V6LlPDgq7kcafFjkZgIrSDF8xqAfypE3mRdCovx5CpGmvANwyRtVz2sPjURIDi","YveZQy2i1y64sr":"h4ANk5bVdqY1KjsSYRQndAZmRnGIBJJiPzvigS_lmXrsiOny8t","Tm54A5oB7wp5YtrPI":"TQrNVm5PVfX9HZBKxEb5","KSG81aq":"zMDiuXFhE0M0fbE0SzaUlr_wrsMe0C5S2OhUp5ntiWc5s3vk2CgEyPbC1coyAhu2XbQq1KnrALZUEga_HNzYrKuNdTUezrE2lkVcqcPh0t7cCa3","qWbDdj62li72":"J_h3xGKFCThmrN11FqiIewt0_PgX8t4rjS","6LmU9V6YyLh":"wSffmfxRHilWYGxHylxKIdAUdtz0X1uB","HQqr9Duox06op0yW4nbJf_8AlPjcKau0TaMkUe_2eZOw":{"w":"FrgkhZJeL","vT":"lVv","bkT":"PtM","vs":"0bpZTpQ90LL"},"waCAUcssZCbJlrB1vMV4Iq0Hc05PeIm":"AUwvspv8mm6omKL5kUH49ESCPNiDO6cYqo3HcUZUVesU0bJE9yE4LKjQQiug","IL":"nxDVGcxFBb5Xp9shR_9rfLZ4mRSe9j4e18rfCp2y5yeksGKqeS","y6A7QyO84ArQgfW9uMDDVvPaNC_Bn":{"k":"t8","kFmsrF":{"l":"WG38FAS"},"L":"1Z0b"},"EqvTo":"mloPvs18Jg3OSW_4IcFXPEo3csUnvlCll6y","900f8T1EGLscOOmJ5o7U_DPhrPgszw":"EZL4GjQ7eX8Hx3ZEL1P7TsJTSa7wf4ijy31IvOzJC9v89W6GDKgEzOyDhhuvB5ij","A4nPO42HD5FCQM":{"W":"qmy_wbK7","H68q":{"3":"mw8z"}},"7itel1LgAk":{"9":"udv","Igjq":"7Cu8rwAZ0H","o":"zMvZyF2yJxVQGkm"},"FysBL15P5rcdRblvVgiDOexO9v":{"V":"sWnIUAaRFk","i":{"Y":"1QEF9"},"dbOn":"NxmcPvRHBPQ","KbX":"eLPO"},"u0svBYKlCoZuVha":"9MMdTvwQ7Ko_xVoamPXlHwZnJt7QGk4Fp3kr3XHKmOBQ7qz6rZ5fAfaZRy0mNGZYb_TBylUytcZsFOt2Ej3_","okc2aI6dqa2Ldy90HRczYJQOtc5Mt5BjVDuGx1uf33_tbbrXBl":{"d":"K_XtGMuOU","V3rU":"nKsKjq2JILCz","o":"9d1fxy","Q0P0b":{"Q":"7zu"}},"PVJzK_q":{"D":"dBYv46qSgrQ6E","li0Hmg":"firLcVqEk6hr","cGS":{"g":"z"},"Xt":"e6nymPeIrZV5"},"DQcWcwpPdxxzmb8bUZzaAzA_fwQSjKxKP1iZJHbSBVv9XIa":"bxqnmTrbK8rZ_3wnkxTuQ3NkJrsA7ty1ja0RepIOEQPMGN","7zWbtrF4ba5D0__afHcRfucG":"heegkFLRf_84jyakBJN520l2ef3Hu5v0Z7ALsmm_tAfEA83Dzq1btKe2a4HKnNhqT6x95S5Hjv8B9VTELmecBd","Wab5PjNOvdMwvz2wAlpLhU15Y11wuytokti8V":"An1PDc7EdJfClhn8lyLT61WZwjISju1DISiV3aaePR","K6G":"QIe9fdp4nVihsak5FGHIuplcwYR0DaZoOpoXmY1EIbRBudp95l2OKIRObfyxXFywsRwmR338DV","INjx1q3Fr":"4WSXPRXT6iK0_DnLYngYs3XQXwEAZI","ILy6JI9I38OiWUxp8q7U9yfbwR":"sNLk0GeseNxZ3VINlhKmS5AMrY4kY","rX":"oW3gQzTlYl7G01ZrUuFc3nP6mQTGYxOJ7gD","fwl42HT0WLkmxFv6wBkEWuFhm":"r69NaBgaqgcj5NVouIp29qNrI09","IUu6pjZTVfVjpAvNMtEaUmcm9ejIhpCvoz":"lWUd1Zx5JES15a3VhW24SoaFQ0QhSZmWvz0kiYk4rUXX4C9njImjwfpIAiM6UoW8xDFwNaw7wu3dtn","qix":{"J":"7WUHO9","oBq":"GEmr","QkDl":"XTD"},"XiQurDzR1AVkZlpSUx6qQnalazZWO3uP3f2q2":"xmM2E0S9UZ9UofEogE68L2PTCE2OM8xawoM6gnUeMuIzthPJjPVj7oXO","QRpT1iciV0llxj":"1hOWtrDLj0wKq92JDkaH5n8AhV5","lnmNXj7Rpa_7_pHXiu":"svWFXGgzpZpYM8ebY83FKJbWax7l6ofX5IP","RWGDQMdO119pVtUK2tW":"ehDkVUrT6TTRSefmOcl8Ny3UPqaQ5Ag9hFGnT9Ib6srXKQbyah2","63ScT":"96lFzehn0WBAqN38uB3zom202wgnC_sWWXOpKBWY03DLZWMH0hLs1Gm3n1ETzPoUxuSYmdd9cwAgIie8yt","lCxIX_8":{"F":"cbN","P3m":"Ir_bYnHMRiJe6IB","tz":"4NOJUFoTX7wVxFzdQ","B":"EchAi","eT9mk":"lsxR"},"3HdnGO9hdAuTeZ1rddCp4KpmWFai3Qsniw_S":"RuEOmaky91gKykpRqvk18jd8mh2oh2Al6C8BAyyurwze6sihZ41lXHzJG6jI0AiqKc3j4yvSLsM7","xvvwidRe":"SJjaNvCTIxyitvDAggqAatGD61V","Xd8S":{"N":"uBxOb","rekj":"F1QD","K2E43C":"8Q1Ph"},"c":"maJ4u5JuuYsqrb9RHthPtmLsBxd7SeernxZM9UTAozVXLf3J5fGpx","ncWdQs1PycmO9aKLS4sWs8Fq5NyMPXS16kTIF5tAcLDT":{"G":"WW4WcejW6EowAu","O":"UWDp3GP0jiO","Tub":"Har","fOmi":"cuft"},"GTfRy54WIqK2TBXv_cery0VLDMaIwBnm":"rygDij2kjuKTP5iL0kkt0WE3ZAnwRt29ln_BqpUAO8bmgZPW9gvt","BBibEWDU2bg":"pqi7LpjiPWVrCl5lvEeqDs1eKmjmCNDiyCWXZixlbRAA","dQwyfaaLaDnsvQ":"h3TGACypFS1fzbZlieb9tfxU6EB3tHjfg2CFnoxTaavILZU8u88ssyH0AlMM","5DKZ6vU31ZEZpqbjTCEXJ8HR3UN9sp3a":{"e":"8p8jhrabxAC3BpDqg","8_M8":"XEGYFcqVk","Un":"VYnJt"},"GZzbO":"JkJT8m7e0nUO3_ws1rEKcZyJNp3ysKSh","fVgN6eGcYAcIoLVSBmjzB4IGg":"KoH382bq_cKqVHtxYPcDMfw34p1B00yLuYA8_KhPhpYpjme3b6EnTjFRyXhSXXIQ9uJEFL","UX4pV614JFBAau9hksPu1m":"2OqyrmMnyHXSO4ttTi_ZMQ1T4wAnm","f20Bv7hUa3UqD6AbJ":{"I":"","qOKaT":{"A":"BOh"},"QPm":"ZW","xIBVRoU":"bjFFaWD"},"AtYGk_DywydoUc":"psfgJdUzXC5duQxeIJuKGqQU18b5LIi1c4xBEWTEUZSGbK6XV7W79rdT277EewGj4","0YWWiaNqUPH":"GmSdNIdMzhlpOHnnvL2KpZRu3QqC82qriazFK0xcDy513_B4VcspfufCqWr5_Icm2qJnbW6mqgQ5oTU","eypt0Q9QNf5swpQYxVRg9p37RUKobnhl":"asWhucJV3K8pm1T4XqNmtKtLEVesu8Oasd3O_9judJruYdFUn5o_DvX3AN","r9g5":"dLxYcyk3GfIyC41Xr4QA9uFOuhxFUTLr4QcX_GuS9tdBeN3MmXEEeikTsA2hXAE","WvUDlgdeG3w7HFLnM2QA9P050gmcxAlg5qXqKEOjmBVM":"cueYEbi2B40BlysUBMz3STCB54T3KGPPZSMDWcXlBYvIxaG","CC5IwJuUN3ysOC0mXH":"e89UkSnPOZbKvrwYhFm94whP1zui8mlMPntVij1PhoaSOIxqXHki8E558uaJ5qzN0vBq6e9HAzGzj_JMorOheRX3daS0IaMb_X1c","gIrrkL7paNAn87tsi_7OcSbdOnKXf3_6JbJzsLu":"cOPHtkVx_A_xjVgajXGECiQLaoW1AwnOW5h1Ei42V","NzuhaYV38ApNsFiVsGv0ht":"VmohA5N0K6GYH8ZfLobhZNNOoA3LxBvrPNABvIKIWwWlAIEyas1YqXhapWBb02g9yp9e","vA5eMbpe7WUx5LnTo63EnV3uLPt":"dcg8x3F5BqsibqV1hAtq8tYCiqNzVqpgVLqIfk6B41qMUp_t3","sjnb5":"Uh9P1cxwXoDCo2lZxSSsFNk4o9Ft8BuOuNxk5eBR5K2jvOtLWH4M4qWCEZObnH9_hB4ypJMfzZXnQA4hKkbT","WDpjxnaWosnJIV56KvkdbQvhvg":{"e":"iPA","T":"hoiXLLJ","H":"cUC7b","tF9yyKk":"NMvL0t","bXJS":{"N":"qCbm"}},"U12":"PHQvmi1qJEequDXmooFTQvXlIMbTMUxO8I54ZwgmY6HPmr2z_fw8vGJ13p5n45L","SmSgf7NHfuWpw8w":"IXK8ny3JfODUBA9W","qcCC7_s1E60E3d":"hdCb_eimiSVbgxulY3_L8DV_omg","B":"hJrz1gVLN56AILdf3lN3YX8Y4DeGLxDqUr7AARD2pyPWzXIvNeJ3vNE5ONga","9abW_yvzkS8LssCZlfn8XSDW7xfl9":"YFY_JvAsMPeRL9TB9rQrH4RDieycv639NNqgZT18WMNr3","qy3ishDNcPIhuVAFAj2pvFvD":"xU0nLeMr9fNFkdkzepiAPhCFwkhFrZH7WiGeIm7nz24dWB06cMYTHe4HpYVYdEad0OmN3yl4pQsTjDoBi","cDly2kzxNbAQNL1WwbsjEvt16Mb0XAVQ":{"4":"6cdImM","OmNB5eXi9":"y0t0PEBIw","W0jQrckSB":"mtU7xvKYfE","Hr":"2Iy","qo9p0NzFp":"9iSR3MW2K"},"SFclDPW_ZelO1uoZzJ":"XnJ8LnyV9M2m2ZYX30CuARDykQyVr","Dz2kJmYDKhK7amgBP":"Py4Vg1ESUvsKyD6mD10HIgwI8XIeRUitp_2hhtOzH6Szw49","A3mEfMKA7Lyn":"ijSfLBLbnAXDZh2QlwR78mk55Q67eFUWwpwxauUM","4xseqafP2GHOTpCJdV5":"nySBmcRrAahE912NXjdeySs","H":"COqdTHWEJVQGAirMbiIP50fgH2Dq_wYswQoKymx_emC","66kwhfOInV31":{"1":"QuwuSk","gqOfm":"mWlLjr5UgAu3","cD0rx":{"H":"Xdai"},"eBRIM":"iEV3SI7MohoUG","lMsM":"AgA3zTo4","0o6":"bvrb"},"vTUJLTIQ9POF":"2n_3GUKv59IBZGXLpsJVB5v9ImzPRiqDhLUTP9hTjufRD52LUZQF4C_knQZiP","9zKDIOHcHzQKUD0bJvf2neXczBfAN5g":{"p":"","YD3Y5":{"G":"gbBL"},"Ifaf":{"J":"3Vcc"},"j":{"g":"x"}},"acs6h0RcWSueSFFkBt6lOoGgy6Bgdmmw84ei0sUCLYPg0nBq593fr6":"LkTI31NHduUFLoVqQ0N0GXmCnQ9aNTZZiflSLKzPqiOZuJddliPqnBxT","Nn29n6ud9g3qzxZEr8hXt":"280skMn6G8hURWuWianjL1hmum6GrD","1e0B94QZM0sBwImRp_Aev9B":"8gdBZN7KcUAER0KRp0lQxm3vdOd4m0wNpDBMp4fl6kPPRipkZoLJCb22k","K1XvVlW5qWleSXS4Ykd8xlm4NQlg":"wrWV3rJsoEpxqmVRMsT4J1wC8wuUd8G","DUeh7Xh6cCPyEx":{"0":"nH3hRaRyepy1N","3":"AQp1dGNcH1OR","VUn":"yorW1AUstrcsC","_ypgQ":"hcisis1ahzQ","m":"7ALd","yY8":"FN","A":"TNCvcsa6","w":"OimWBP8TR"},"HfsdfgSqyFq":"nPxqdC26U1bJKkadO0qYc3BvOcOUvLBRR2SoqtF_uIDC1GIrmJVALBujU7ZmY7_R7pO9R0zkrvWF8g_4lJiQJJJHnGjUy83kRqUgEZ","5NK9Oq7s":"O91q4NNHaqCfEyG89ObCqVQK8Biu6QeMX7hAFc8","BmIkGgC21Wff1Jy7aPDc6EuM4xyIBImyjoEovWJd1KEqvorDoxGqTmF9":{"Z":"22FX","_":"ozsMBtl0EjFsPH","Yj":"22x","S":{"B":"bJ3E"},"wiElHOCtqr":"PIUEH0bhm"},"guBGcv2i4BC9zPrXL4QfwqsMcyHbdJBhlWWW":"aPwj9gPsqMaVfOLunzmSh6RSSK9kTPrfPpdEvlSi0rx9ujAi8ykGICIxuF_GYB2k0wlY5SFvOiAGi3ESmlir","GxXldw_p3zNaK":"8wqphcpGCtsbfY_EF1MdO2ek1b","Czp33PoXMg3SrUYrRiXZZQ":"SgaKwu22U_KLHfOHPQqqiigkmfi1CAnxmGWFFbn","MK4jPYD_YlJ2LOjvNg701mge8F9PwyZHewrOxD":{"J":"QgVG6","PXZ":"myZQmSwHdT","Dy":{"Y":"hq46PKjSFO2"}},"IUtSP1kojZLN4Cjc":"OpX2sn4gkWfqPQgJdEV1kHSehUsYpVLEjY89sF","XSpjTsVBvU7V5Qzh4KtQ9ugmAe":{"9":{"k":"LfMRixLNn"},"I":"DQzgyIGcDdtQuz3w","V":"VoMYav"},"HrCBGU":"TLNssQavrHJWy06WaDYPoQrxlxbVlVk5pn09KPhx5pQ","wpC9NU_Qi9AlOXIzJFzCG_ddsfb9FSEU4NT7MjdzGoCl1":"LlkGV3zCPspLc1Z8UGMvM5U2eMm3RthaLiqNYEdpZ9BnUmGfwmXM","4tAWd6Apw9kOP183HpyhCkwjw":{"c":"pJB2ST","rj_QXVNy":"z6qN1wT"},"4He_YmfBbEoe":"ghp13qIeCLBbYwtcS2qz2yr38MdIsDMBp2_qFjyaOwAuJgnTaTSMpRglz5s5SxjLx4nQ4inXbYh_4VdN9BTAkJZAcJfTtS5H70Xz3Ll3","TcFozo_b00gjWtFLeGuv8x2cRLeIZ":{"D":"CanDfNgOeTopboW73","n5Gr":"H67_nRfJ2CXXdXO"},"6ldGBPXofZy2CdS":"seOt26E43su5bLz9PPxXQyW6VZH_6MhrgXGgt0Ry09fwREmPnjkZt3TZ_KHoYU5Ev0F4uyOp24MsnTOG9bItwNYIaqWb22SPP","2dBq599LTEjMwqn_bVDnsbl":{"m":"3ykvO","Ta":"O5_","NCv7yyP":"ty_MsO","qX4dYH":"LWSxtwrzEnQJ_","ZIJ40N":"75CySHP","4vX":"mq"},"UE530Rh0iCD5KZfB1Da3R6o5d_kJ_jzZYMhKix":"euBnogXZiH2CVI9y6J1aJhYHeZF65960qgLiUBsFy9k5JcDOgi","S83vi":{"q":"K6nybV2","bT":"qn2wqa","PR":"WJmC","eBsjl10":"GsNogd","N2T7fvt":{"z":"kIX8Mt"}},"iBjs0PaY":"fmfRG2uE2xujXkVLRy3sjZzjRGPuCtcIkUs1XEZIdG1CaZYbiGNXgeh","DHHOZxS1Ypp_4i":"BMEf8rmiStjI0onsnXxTyv1DN3PyQkrRtqfcmWJ","K":"5QkJ42xOPmT4fyC0mMaudAGXNrSlLx1vit9Lo","EUMvq1DA8TiYIEJgog":"5hubMk4ATctvT7QAsLA_J68Wp_K","XLjDTYgrNnCFA3pxsj5iO69IL1":"MOJejSnjTPeXaVc1QDNq0Z1y3mNUNqQFbs","71IRphF2ZYhSgViSsmM":"wKfUefL4tYJvM3MQ6iclsDPVhxJ9NslsMq8mLJHCi4beTxikPnOKKZpe8UYsTCHBQgxdNVoUqa","Ajhfy6d66rwhYTHKEfWLje7c9ndnE":"dvTz4vaawdyyuE5w9SbsDlovq9RqmhOY02CEQiDuRsAabaGp2VhwMiDafMtwgbzSzg366","J1kOA5LkPh3srPaMxpXrzsk_4_7DP7M9zgeWJLog274a":"Iyt6kTxPcs4LIVsxRG8eE4DcpsWU1W7YQ6tYpiMAZXGVVISNSFclO1hgnxm8MIanTi","MgyJAqc0zJNN99FEK":{"2":"X_MNW","y":"ZhKmGeUImyrJBN"},"IChKfaz5y3gCpZf":{"o":"","AJU":"auqxdXoH9TrZp04","y5":{"K":"IJOIpeXTuNM"}},"qggtJwIvUgwi281MQOlLngJFQwTfgxX8jzaVw19x5FH":"h0t7hwZBzNIAXzszXQ5ocGW7ydNIAipFRFN_DwHO4rw3XbXySuKGtnMdx0hddT5fO4h3Mqi3","oc6OSWlCh1DhT4w3KXq9XBbUYtVG3jYI":"vgN8y9cg31Y_XyMqWJwEJncM6GCx6Y2l3c2UFYTnMMbnlK","qh91t":{"o":"UahsSj","W":"EuaNfPl","Mc":"s0w1IRmHIOLwX"},"fXpCJ0OSxt_iswq":"ezPFYZeb9tj5gTIOzAgcGWiKmue9kQLpsitpNCeJoaidH6J_2kWPc4vzSgL78qmV","VpBFdrd9Fa_svI1Zw_ugMz8uY_jWRAwF48ebap_eBbZ0Kr5":"jagEWLIOb_BOooXaExdLuYRXytsOolwNlun_wakk6AVtj3MI","NJMFRXzJnRerJ2_oOTlYXBtkBLIH3s1KiexeF2ctQZE":"aFiY9d4F9Hkc6u1zyXeNK1mHHaoDt9xkUKiMaseOG7cbDXCvaJNsNxBMg","xlC4A3IniaHTxckWckqYu":"cu8n2boaEZSPHNsCbD3ptzCSE9z5VmeC0fa9Mc1psb9O2ONHQIVMY","q8":{"x":"npRtELf","pnDiwTzI":"_UgRaLY7","mU":"d_lV"},"GNJWS":"VE4xpeUB7lYKsvZgw1qL9WuY6RmkZpsQ70EVpM2OHgKz3EevX0g3FT057SKfXGmii_dAPbo","qlJVTwJScUBeORkvxqj":{"l":"W","Nfj_d":{"x":"ABY"}},"JwXJu":{"2":"","_n":"aTRA","42_S":"OjBb","VcUd":{"j":"YpQ"},"woStvr":{"Y":"Y"},"JtlWJ":"wCc9yx4GGr5Hhs","Lrqngj":"1A60N45"},"QBZLg57t9B5UYX4_rM73xQamhzA00fk_9W0Bh8MTroadsNJ":{"1":"FrCdsiV4S","Q":"W6SBe","gF":"CUYNjXc8LG67XA0Z","X78YzY":"u8UIVkqdG"},"oYYSTvwv4":"2Xp1CoxFPqbP9Z16C_EGBeqGnzcb1xV2LGyQHr0WqgqvpeHNKB9","gZgucMbYJzdWySkdnmhZ":"ELRLPyCo3Qcuc9VIbEzBkZJ5","owlQyNVOtDdm2gBJRESOkc1sJ37W5p0Ul5wA":"qTiR0arALF3EgsINE_jTHKv_S9USU0WMgHWHl0175B089BR5ADtzGxyFCrFNAMHybRMIosFO3os","nHIsjXTU29ghTmr4YY_23Qvi7_oZ":"rTsR03CoTWlA_nU1nggKA7dpiJYHYEHjCV","Q6jWdd":"aysqKdjyjjTvCEMrUx7CEnvt27fWQaWh7s54i6pAA","6pJyMrcEx6_zba2K7W5r4Uw4VgiJ5GmU0MnzQn":{"A":"aS59CDvGVy","Ia":{"v":"G"},"zFCM":"g_2ocSyyyKhCT11","KtwY":"u6ledLbe","L":"n2miWeCH6x"},"94mXtl4IBRDa7bjk_IrGbxlkNWrdkHIXULl0Vas5a2qiMyxqWwD":"tArpL7OETFmH_xVWUd2KhFhsjjCl_CbTuGHLAn4uNXLasB1ETmzJnWO","nNKcAezimxrqcnS5Yl_EmH2JVcNSgVPSDu":"7RqBa5NvnjibQg1DdNL1bCfLIbghXh64phRaaPhI_c","118sEw54Akrrxi6hqQ1rV4iBsqvyjWJ":{"A":"Dci","1aQ4M":{"4":"u3tJ","Y":"3x"},"J":"oXstE0ZDm9mDUlcR","LgM":"zw7wCA0K3t2WJM01"},"kpWmqtlizEuSeO9PyM8rt9Dgerw039NR":"1RQ5_BPW9ykwMuGUEiNiSAMH0BkGHs0MmpgBF9J1CGIrirBuBxE37dBFmMkNjxCI0O6kmLtXhSv9uJb6","yFH_IBeVTBxP":"sXxO4DhFUUU5rjGiPM606gjlfBOdOSWV8TdSsp616HMkPh9nZVOb6tAs7LgKBoNztPUXzTCTdI3epxkz5SpbZiJhRx","9SB":"i4X0o2pOxcR60bQgvFwP3XAdT6Ab7Qk4wcYCNPIesr4B5eGncJ2o2M46fQZsoOvjjAL9mLM0P8ktO3m1bG2WxYVB0hH8X5lNo5od","MXQMMKZHusUzjpPxA2oUCNamswLv":"hHtoIR0DjidCVc07FxIeoHjmqYGmT3_6VJ1r7vOyZZv","DI3WfmSN6JgjTk2YAUKF3hx2_LFrz1xKs71gRNaMywjm1":"ISWUkpCEMKY2_IYx9JJhDQ2COX5sOE5DX14zTJuA4trL41GUQP7Uyy_F0_63cO6llLVdA9sZEI1","SVs":"0HFGx1VB9P8UlJJ3HSEL1dJE5VPLMSowAm","xY4eFkTr3MCyQghYHZ6Wb8Jd_KXbL_":{"V":"rHPULpqx1Kfj","B":"D59zh","HLuE6lFi":"yMNFAXq59t","gbepOY3p":"MuY0sGf","I":"Ad69"},"mswu":"1KXrysO7YEqHIDKEI5zHfUtdg4mhgh3lVkqVSv283zZ6V_wuh6Fg3iO8rfsS3qWd4ett8HQ9cC0","I5c68nNe0OIdUIK7ikZsH":"o2ng4KlJrdsiyVKKKps1JdVaM1g","GTHGtG6K12VQIFY2gc1_5MRJ9P3ZqOlmU94Hhui":"0tor7AbXOfu3a9kg4uKwD1NbrCaJ2iUGNWXfiAzpZ4Kc9XN4l","JOLAt1":{"u":"AOoPW","YmiS":"QXRmiglpZWI","VjQ3s":"NL8X"},"_ysbX":{"i":"4ilqT0gx","rhU":"oeucIe","o6":"X9c"},"clt7qrxC3WPU0ua":"X8uFa3e9cpCinP77uXZQyRg"}'; + expect(instance.data).to.deep.equal(JSON.parse(expectedJsonData)); +}); + +test('06_update_cloud_to_device_large_size', async function() { + // Data for this test was generated using the ledger test tool: + // https://github.com/particle-iot/device-os/tree/develop/sc-120056/user/tests/app/ledger_test + const jsonData = '{"46":"OBTzYnP2zxPJ29OOKMcY8Z22zAt632KVsEmGgWq","69":"3kp_0zyXm3torCeJa0IrQuvppzONM47uQSMpLPnuDmoZp76jSAidStiO4q0pomByDbLgU75ADtAVitFGX_Km","O":"bMzUnYX4Nm0M6hXgDdbyZyqX_rMcinGhHXkiRy81UsI1MAR8G","2KAqBa8mOS1BVklw1s77WAME8H2LO52CEXAQQ1e9A3jBdD2Fv":"yo9Fq1ZDxtnICReiOJGCqdcSwbjnURO78s6R5Rr_U2Lx5JQ3AirrBcKvgd5vkPQXRqDkfC","GUDaD57DUHH3bym4y0RQ":{"F":"Dr0RtPP910dVy5oYy","1KQt":"tOG"},"VcEH37LUkgJFyOP7LuOuJ91OkhkDPHQ4U6NPKRppSKhEO":{"7":"I","tnz":"f9ouSlTUGWotkWV0","DL":"jJJrnQRVOF3","z9":"fsI","0F":"eFHas2gYr7j7xirP","HG6":"Fm"},"p45DuOW4uU":{"6":"OsAzNZMS","C":"0z_F0"},"DujEdh":"nvdu7dniDdV6UOuQrgzxLhfaYsL0BgfhPDHZbaCtG7ns0HPYeDiNukHUA6UKvoVmWNfojPfvYiuLv2","MwWowakyuJ1iLFNXQDdOpCYc6mPKxhPYNy":"Hahwf9oxoz7Xj19gMDoVWCRLWNwwjqlAzHoBmj4rxHIes04OIG5_foXGxfYlx9BuFZYAhJNRI0H1VQ","z4WYjBx2diRtgJUvjtPNtFnw3gTRhHaCss0DDj6":{"A":"0eTvxEkfXXv1RpYNH","x2ft":"PbbXf10zpRi"},"uyiLb":"okinJ3OtzD8PUcqojL8Wr5UTOcI3HMQKdZfltLHwbRrlWcHKA8B8L3","iIlEBySZWzRGx_hkR9BJadKPE8WL3EJKWwU8e2":"X5loWs76qQUpGNx5lxHL0cnbFszAZtr2Wu6m90U4ZCTA7EyhtihutVs2k2_ZW4tVaOdWWmj_b","GN":{"6":"c6hkgom","E":"GAAW5","hlz3tl":"HxsNbTzu","uZ5I9U":"LMm2ECVftcO","Aeoz":"PYDvl7_GeCLnbfiW","iQ":"PqXf","9IieFzZo":"kiy6r2LCd","v0n":"_ZcQ7W"},"6hUibeZ":"WBsslp9XirPPUWFBfJyIiSEvFLtJrKmq","T":"3cf86_7vtfDQ99tzeFLDxD05xqp5z2KqSgMW6leDx7TjKiij40O0JyhZR5XhQiHSq_VwIvHCKKtsxZ0FR1gz3PQbxcFC1thXKZdrBA5","R7VlqJWaX2xtec4EI6dcjJZT_ocCib98Ej7xSTn6":"FFxuOYpJllommFCv53yfQ2i0ipKvLCHr7GlWJnE4QM638s9gW2ZW6ZTNyniZuEr855zJSlIitBBU1","cA":"i4zAKRYFlz7sQ2sDp8FtBUdYXWL8kqTnnPKeFsTTgy7ZPFrIkFIdN805pwj1_RAkfbwfTBFDzDhD98iENWDNX6EhCVzIdUVyqVTt3Bz6S8iRg8PEeB6","dmdg8ix1zDW95":"aTknoyXzCAPQypbyXe7CUnbFlAmY8fnvGktGRV5ZZXWaecQLgBVfpuOzZGEWhEux3","dpWXAoKds":"rPIimrWWxLed1vUl7qOsQVe0jCdoz","eYjI0_zRegKVMjxvtM7ho16v57":"V6eyEEfOEmlxL9fT5HaoSLS9bwtBc9oTvTgsQGyaTFD_0Z7jfGj_ZFwyuxRJwTnrDl32YlODEW","Y2":"YZOfLgfqxzMMtgYMjvaPURq4Omx9cZcYZ404dYZpIQHCwN8orVFZhkMi","ggCKeCiODdW7IP":"ZgZ0vsA0tj_62qjfRauJExL","oh8KHIF":"ImkBFNWBZd1jvdmYLZdeKuLwain5lSSAMFA","xN7kfSdsJfedu8pIHHULbEg3tLjC8Og":"aVIK70R7VqpCfB5WzIZ55xaHwJFLrwdYui9QTlxgHarWMH7tIWwIYMGahJY_xKC7O6yuZVJdL3ctzM18egyH","8gEnFziPePA28":"77C0bxz6KVWaf7cRwemC","0WYHZDvoIpSfVlUhi30rsyChqr":"36FLJ2w2HgePp183rRYQL_eUYAYENVk1hyhOFvQe9EVkZLuIe2CmpYF","nXtCtn9o0KbpFUF":"oKgYbZbIb9BBkv1jFsHUNPDAvuf","Q71ZfenN36wiX_WAvP":"2jj0mcZIdaH5XVDyK6t3TGrfYtqbSvBsCugF5J3SqSpc74Wp4pexb3gLij5YBmm2lI0oYE7UcowXcyE38ZTVhXY","1DdHGAlCOMOlJytFcEQ5qTCu17hvDbwv8z4Z6MYI":{"i":"IqSlBGj5LG4IC","M2":"4oG3JdGhEafKl7em","O":"uxtn","ZClrv":"eYu7","RAm":"eS06"},"F016Fw1AXffdy4YgjZ9SjJ4lJrv8YM4ODecLWg4":{"l":"_HmD6","f":"i6Wck2NxL","Pj":"Ga3BUg","4iPwZ":"5Ger"},"19OxSuhQ21UjR6ImRFJSjHZkxE1s7uonW5VpQaS":{"Z":"mL9L1","LLb":{"w":"7AO"},"d":"U2gFrKpO","m":"uTrumgy6bQI4P9kbD1","axY":"tR6r"},"dPWOUJSD0fQC07iQu8KHdwMMnzzQyVSLV":"ODMFvIVMZXxHMVWMhUuFliCWy5PTdYlVYgeLy_u6QAPker","DZVIBIqObBdQg5T2x56lo9bH3NFXnh2wWf5PE2Pa":"4c5vK2z1NX44paRBHDKJWpmDXbIs5AWn8CM4O4AIZnS12JFj2","oux8O4":{"d":"CxKI2YVloLN0ehI5FqCKx","87lJ":"a1N1L8W6hF","z2k2":"g26rBH"},"qD8RTz":{"9":"mS6ywKAI1VXvw","B":"MFbonnFovR4ItIFp","gMey":{"3":"9dttLNFQ"},"x7L":{"t":"I0gzV7kwe"},"Yxo":"cpIOO5Z4Xd","mh":{"9":"Vazp"},"iCTTy":"znt2J3Tiu"},"Pxp":"W821_pcdTlhYq9jR91yvxSCxFPJHg3EDWzXRaT8MeqmiNSGx8uDXDSlXB9efXTO2p4E","y9kYlQcv2":"XkqsjBsH4suhILm_XuYHm7ekXg9di80WWgAX2vxca58iRPix0Kk2JEIwnNgn0rBg5UDDyLyYpmkVOnTO2xV7CMLQjmUvRkKdbzLcaa3Og8Ig","mhC8psN19kdWWZnwZSagzyGAPO":"ate_QPmBM3lugp32rxuvMCmVwW7ZLEkOqbUeQaVJIZin9iCYCwlyLyukoepkxKJRHPLBkTkH_mpvp7ySUJyUjA0cp","NPaCc145JL86":"7IRA2Au_Z953rqA98eZeO9pUiQj6zL7EIQkyIRRYp67WOxR4TYOGAcjYk__gWcxAjw6G4kHPE92B0o96vsTirZSN","I0GXtjWBimbBS":"9F7jhgXrrIPPmUB0mJ6VhObXiOT2aiEeThAckBIDLl","M2HClApM46GWjWmHMrq03M1LMBTOiTKFxK1JERnLpZC":"dK7zLNlbJBF0S992518fvJgjpwnU1W3K5UcCCW_IZkFHBd_8Vv7IkcLp_Y6In4B45MECpx","oJ_FeJ8ny6kSsltdxX5xttVk":"6CN1Gpmtznm3_o3mmRvbbp76hgttnO7Nhtlko98oHK7iFBayD","wrSPtdK1hpsuIt5R_5GXxThV0spmWreNfz2azW0PFMKMTCzYFz":"6rBOJZw1tx21wKCb7YqxOIssfL6HefGMvyr6S1e7xJqCdYr1OWHlhxq6G3RT8S8cJ9R9L","i6VUQkD8jPZAPHjKI4ZJqOCt":"vwU0TMmkoxXSIJjebPnZrhVKTSmgHyQmHQ1TDicZoKcc2hiTTdngxSG7qAv5lg","ryj":"zRoqyomGFiwNgP2eJP0uygQGR6RhEFGUH3LbGfYJMmWMs0NtacE5zlYOaiGRQVQgKiVBkLiCVe8GDXY0","ojOe84WP0Ed1K4":"wpcix6CFDXxISNNeO0M_g69OWG3","rtvR0f49qcaKN_TKjJ99fWB8J":"tg25wNeleqLLFOl_SMesjj3Q0mqhnKQ7kvTlzDrqhyWFDZ8o0","hobFjn5KgFwMD":{"I":"e4KFUvFVsc","0_YJ":"DRWBufC7JVGOW","equ":"Z7VgoOJtRES","C":"N2wn8OIWgQ"},"I":"qqpjZlM8LEiaUMLK6CcY7Oz5QdUJ9jMS55PDrCIdJuCqQoZUKEj","Zo6P":"0dpMzBdXhPP4RSeDfnHuUYsO3vL_vTdiTniVRU","FPbilZ1PYpMzb0EP":"nU2jK5b2geCnTnVVcBI4DdbdwtYARwR85tgROygG0y85No0wSK0RIqA5mjJkGsQnBObOJ","hag9xcNbZNN8Wm7nN0qHzPFLGPzx":{"A":"IaGVp","8Bm6KXh":{"3":"Ax"},"fTcJU":"9GHZij4C","7r3PYc":{"h":"TUB9tD_n"}},"bVL8Qa4LRpBAvTHl_cRNrwh6XrROaG":"u6JO9mVP5Qc626Un6UXpQ9Y6VGNPdAXIOCGo9Ds","AwSTZ3ORJGyhCacXCqyF1EWf4QlsbPPSFf6NIW":"AaGr24PpKYjXul2wyTpJbBunFt6hvybj2TkCZHJL","F0b5qNi2tDDPIzIIPQe":{"Q":"6DAxa3","vbzMr":"ZKYFYo","l2":"adjJ0OK4tMh","8e6":"naFH1dZ","e7mbA":"YJoC94GF"},"apWyvKVEtyCbBaqDrq3":"JmjCaazXgWTQduQHR1Ogo4a1YsiMa0jluL2jUHP5ZBBdXllvijwP","kLnngp083ZnnCeb35CebDcw506hxrvpZ3X0dCW_oXkFGPrnFgj96r_v":{"G":"GnDH2iyTpxGR16BJNUW8","GWB5MC":"iMBBXFzWe7cak","B2Js":"ZcVg3NqfmGkr"},"pGb0NM":{"G":"N02A3Xfh","c":"HXtXJteAlIqqd","J":{"5":"","wo":{"X":"Tr"}}},"GYXZmFy9lreNkvl7KDlNqIlJTb8o9P":"eK67KCMf5vNbVJSkiZ6yIjfS6T1BS7Te5iDB5vTFjNr9y43tWqpVSuU5UMhLQxo4_UcipgK8uxmzjIcWg","1148yy2kzB8pgJdWWECkufJ":"XqZ4IQYIoEN1QSaxY3pe47UR3JP5S4ji_wZI","ne78b8ki":"QPyZUhzFvP3oc8tdnBr6aypkKjegfpqxbU9YamNWtf3rHuW0pCWpPq449_bf4Cma_HGflvw7PZcMyERaRH","qiK2BpSQJMah5k9hjHWFcx6T_8KEA4VywfPWPGcFtX":"rOpYC_BfojQnHpeiE6Kme8Kq3azHiCJEKvj2XbQ2soEWbxNURG4vCRmW8XY5xI","Kggw8nGRDw6vISSPbOK9CmEB0l5k3Ih":"nJnRWpuQ_amJCkK6mjTZX5KfM6Og9SJkYEKfnOa_D7i9Y","4ut33F":{"U":"ptZe","QZW":"yI9P","Vb":"P2EHghhDCZVW"},"YZTJ8CCCk5kQWe":"phIOoHH4rUnrbU6v8C3BNMBsErpZ","rkUzc2Fmk_utRiGxQjrCH60HymznB":"llAHP8fb8DyLYhHQnVROrlgL2elDCVCdPJOqpDEPPu2Z2yVJG2zPri9onKwxECiHyMzozA","p14rnqkXtvq8J88yE":"dlvRutouu7kc1lEc9gwbEtg","z3g_yIqi9M_TwKZuAJc":{"u":"MbX","xn":"ZtfKLJeC6CH2m"},"NG1rMiZ3updX8":"AvxddDBWzUrJscfK0mJA25Xu0xbFrcLdIrj_y65SUEjfCer3CBtqIVzkL37zjmSWeFBtPbgDtsJtPUwbMOw0auNYaD08","oCTcpxehn8znwG7DlztmuTU107oXhyGQAC":"rxHQ9R4nE6PqnVhTOUkmbJAbthFNEvd7_7VhKh_bPFAnfHeNZYd3Pf","_LsTIClVQR0kHyHY0_fYYwa8Fu2IdMdPd1":"XJsl7GqwvvOj7dGeKMFQVzHQz_rRU93szcOYwp1GHm7F9QfjPY9xwMTVks7_wEQs0w","w8ia":{"r":"Z8","mR":"Qr5Y3KC","FXwAht3e":"limTqRFC"},"ndfEZXRINmZbHDjkV2qsK8oXnAZ89cRcVslKs8LDBBLPGy3vN":"RzWRDGozBgFDx4ALwYT4rFuObpNUjvPItjTU2kG5k7Itxx2hIYRVY5l8ev03Ndm0x","tVaRC2skgo9eET1VzRGmWuKP":"777wUT0z6bVs3jh9fZQ6ecyEZxfoetTgZeQQZHiN","dboCsyfUaM6K":"bBpnHYJ4D0cswNx1wrF5_sJT7kn5DoNjsm3f2IP2dlTDuxCPDLSmiH_oFXqk9diSjDN","n213fh2g":"WRxsoklrN04_KVH4Hh6W1QhDLvL65l3xdhxcDO9D8q2IwAX5EOJBbz9u4ZFLC3ibx7_FLXdJog75A8I7HRKGyFrUU1R","q9nn3T1HF8lidcM":"8u_b6qsQdp26ZOjDky8ar6XFq4ZFZxyYxk7EcRITAEH","blnJwntMPGnWMdhwLbcOGZYR":{"o":"u3","Ebl8":"H6Z15Rh","_a":"L9P6","DBRwtu9v":"pQK0tQX","fNFq":"vzfd9wt74xH","218YN":"cYiKSv6"},"AR5r_2WCHne":"oO2L4krA1POcKY7LTL0","PWBxUVUEQ_zGZ9VcFdIBUDBPyXfQxHYJh5":"RCW7Vy8HY0jHHaWPn9BMrzQvl1ficnqVDaeDLv1zwcpo9f4bqd9qshF2qBUhlvb2F9mK4zaYp_eRSDVF1uJ","2FHl":{"3":"","oEG":"SYFd","UH0h":"aCh","iosu":{"6":"mW"}},"2v0":"IZ8shBfRlQkJUENLxA7416za5oihVakKcVkzRBqS4UYyd80LUJ3lE","YbazoydrCdkE2gUj3uZgvCElQoRwDdL2YLMfxikt5pkA8T":"zsqCp9zPlTBIJSqhsNUvT_etTbSKRzWZcLwS1VsEwNOkrxrCjwSuxP69","bMDB_MnxZFwhWJFREwIR":{"2":"UcYwfuIjx","8JFg":"5xBiUgoKZp9y6R2E"},"G":"j6YPfOYWJgqPVypHt3Mg8htktO9cs6GHM0dWnJKaQQLeDxFcl9BSnY60","XKtgO52R8wAeZMb9_":{"F":"CEhEP_","kp":"lwxfJmBL","jw":{"U":"M61qDay"},"U":"l8dC","tbe":"jYsS","fcA":"1VRIA"},"qY7rtQNC8KTzGIzLXKz":{"m":"hrhXDpDrVrQrLBYTKJ","Kt":"XMy5T","V5Hn":"zUm","lzpDKKJ":"POQb2sxUWQj"},"HHqTS2K8VSqp":"s01e7kFnvSkl_ofI4cZHreVGRcGjFcy7iPodQsFTK_sAb3Izrqa3do1T","66_ybhdfBLgT1zF0v2peVMjInR56ugDNK6luMvg4iTluD":{"Q":"4kdoHeTLh","kJ9":"chc","q3U4oYn":"JxBfOX2F","DpWx":"f5Ee","k":"oIMQU9mlvCFY"},"T06DaHG1":"E6sgvy6JZxukPop87qibOPGr1EtzFfhM3I18V8SUcuCqOWQFCImnYry0zvzbQb5WJkn1gQq","1JtP5LJ5P_7_YRB":{"T":"","mDA0zonV":"hIUfsUbT","10w6Y9vUu":{"b":"XsUR"},"b":"8AN3K9bHOoL","X":{"T":"UOKg4euCHM"}},"81HYbEMr":"NsvS2nnIOso_SSCVQvbzqFeHB5NUd2scGKMqkWeV6NBdNqVZT45H0DGNZgjceSGWP5uxyjH3OJr172QG","kjdneQ65":"VSQKkMq2udURt87ZoShXvuQUlRjUbDJqqQ_By68ci7cZyA5A","1Ze_Nj9v__lFYetip9GABicWgGcnOh":"OEd_46GwDv4Uk4xQNx3XctCN2P1aby6DkHMS252atF3MukS8MJJfT4","X04DLdlnOFDfvj6G8M_":"Fng07iblAoeWPVFmvGATys","p1KEnODJ":"H5EmKCFvMLxbV2jThT4AWmgW3nd2q4tDYjTWl1VWrzYJa1kfi6eXQMu","HWKb1rUyNUaeyqEgUrqXktR4L":"zOBPz0X7eLmn8XYRieRVg5vLqHt","JiPOmrzXhEr_sKCEzT3uysnMZskEvwxa2sws":{"8":"OJnLR","lh1W":"zj897","0J":"WpCj","k_Jo":"c5wmgG3BvMr"},"jB_oA0wrKAMl435YNtgtBnw_":"lCZHzuYhv5sjqCPpYr6oqvLqO","I2AyEKuNB":"ihfWAQ3VVe9C8zfrRCrOOB5O1hM_DTxwP3ovrdF2y_ca14o22rtUePUqGlQCOazpLdrY","jqPGST7U8Y9aTO5x67q4zHrL":"Teyr7quG1ErzN5hd3ujtwrJM3ByfrTrHLl7ek_Ufame8BCqcfYqbTyN","ESml2vXmSb4e":"a2ckf2Xl0zddcmhxszWDwxiNZF3PfN8bfLEaXl7amX6c6FFE_gIaaOB7_GLEC","uOTRG_3BT4jN64pHv_UWo":{"D":"Sqyme6am","fw":"wcEQJs_v_","a6m5":"oiT4LX1","c1":"uIrrvc37TrsHPbW"},"QAe_VtmwehOs25mpfglY8IYEVu":"TXT7fZH7ZAiM_yQw4E3kffg5h2OWP7E1eZ","33fPcCx9TaK8Eg":{"m":"qIPNMkeIlNaF","MWk":"QZ","YJnZ":"n2J","Nf6":"Bgr","s6Qg":"exYMRMlxAPY"},"HeGwR":"cD320EK8ep77FoRgCnnifGHZjOLpP2rRV15ca1P7CDKAdfeBZS5rhA4lH0mTx_oHrZvLI","2QxFMF8":{"y":"Z5","k0otwm62y":"2ytxOwBqP","d59feK":"1yzApFePqZq6Lz","bLo":"J5","hVKn":"n1M","PxCLEse":"c6uWEn10qDq","xlxL":"nJeVMUwkUN"},"pygrK_JFlDfMWsTPppW7L_ndhaQMkIgroY":{"l":"tl","Q":"5XFWeym9UB8","XYioZ":"thFpoQjMRL6Hjq","R":{"C":"iqk"},"0235KAXL":"etSdS4MLF","mow":"Cyz648HADKF"},"xqHC_0ajSthAxykJFno5WLt":"nGbjQfJE5Dg1_cGwZzc4kZbKSnn0akugZNEeD","HKsuCr_69jmZLT4Yw2ZfV6i5GRgyqDKuCVcFMH21Axyn05AiGRG":"Ydh2NhcjNZangFeN2tXqz4q_HJViJtXwT4QwvP2HvM82vaQBTjJ4AHnefspCrtbbhP","DJOTZRTy3jSzHD4Sfsimo8tXr3Zu":"PhsnJCIBaso0zfzpZXSqy3pMr7IDq8RoNRMBQHRlMHWi08qfgxrLWM5ezwl6XG2Z7bD","ItaTa5YiovAzutY3":"IJfgS2RepHwZjlYnuNpufkrzleu96OmHsZNXMjl8xq3yE4fjDeNTdsyG8we1L1mQBqNuz8sZ","V1P":"8JivS4KMTSnXo83ag9fiD3LBDNMor2lSqDpNsh60Axqkhg1","nY0d0UztS2hHVJwOQL9yGLw1Wf3U6s7I1TzccR22rBrhFZ7":"g9DHTHYbCJpP48VsbOsippttTufLt2WISwjBl3leIUDCjy_mVYVNddfhX0VJKd","_vzwR5n":"UNrRMThFM6eZZflX6w1Q_Ie0yo_eEFLJJu7341yyI5dYL0Fbfh5jFhqNlnROa7S0df1F4OGfKVsMV67Rk9lYy8VD0","IByFvcHM":"xEqFRSBTr7NkS3Mov0gwkFUJ3PtLElZ","Y5cdfD0kTfFoqM":"ikgRv_bQTvuGz0pEc_bTfXyt9Zt9seYtnunC9U8LT72iH36KaTxB86eV5DKKotmccNymcNW2Vv3UFlmsHh","61_aLIVdZrjTQfRUA0":"8UqQwR5p3X2uzKI7NNeIlyqwrlS1LhBOrmOZSUu3G0","iW4mIQ5xI_uy231hwfbPISR2Jp0aW":{"z":"x3zp1trejXEk9q3y","QIE7Bgc":"aPf6yf3"},"bAybF6GRnYGilebC":{"J":"Sj","YyK6":"VOfo5Vlha2Cx7u","8kyh":"6bxm","aQJ":"37","f84":"7G9oza","S":"YHIxQckm9dwwh","1Oy88":"l90fR2"},"ew":"bCKrBngZE1jiib1g4UkiOn4oSeVX_","ReP2QMVCJfswYVM01YWjPJce2":"noEyp86EtIaSTYTKWZyDR3vMBr11mEHrlCMNfptGN","87nxy5hT8EPMEuaizkJ9zPG9Cd":"ZrkuiFXwSCJLpsp6hN9AAxXv_FtmLRcOUJmzWVNBFU0h7uy_uWU8GG4YkU1gq2d722tS5Fddl1PVHbp","vrzA_nkd8Xxk4wO":"p3qOtwSHnSI3sktm36HXghWeLekRQ72HsoXfT","L28":"z5zOyYXrY7R7RpsjGDjoa83K2DJk5pHwaCKadfmOkd6c1uQKdSYJ51dmSHSzpciGeOhUHIp1DXB1B4EoaPWLnTniK0QShP9jHKdJ617lCVsIEIbT1A","Ie36oOLI8pox":{"b":"zem77","TzCVOx3":"fpgFGN2o","yO5YEh0v5R":"2z79xP8LC9","1_":{"_":"iF9ubw"},"ncu2yeX":"jsnfX6","uE":"Udj","NmfUhBkuz":"MZptwbnMN"},"OZ6yZQztQsGUqqaEaLuUM_Vn6SghkFYwcl":"I4Jl89enXc3o3pPizjPtOPuj3nwJctYIq_mmqTRWe7CAcusJPj","Cg":{"z":"","c":"8UDPIpsm9SQmL","ZvJ1g":"0aQml"},"yrFqcqO":"qBhKYHH43y4ImHeLrUl97XJqdJhrsiruDT3mt2nJ1_K9rllri4oy19welj","kmvwXLQT9ZdIYBqmU7iZPRh1JDf":"JFx3nFsQ4Abzb0aC7i847KW2VRdt808oadVDh8QeJPPUlaYV5_ESReA5U8nQQ2VP3al_","2iQqvSHgTMxvVGRjbC":"5pefO7ravivliutO3BthwLjBGW2X1yKSwI54mVpdEhUCh5xxR4aXakkuhWJJVjy43LBXB2JCo0HT","kT2bRFQPa_wuCFqkKD5uKFxCY":"7ISERyoJ8qHaNgnyiNgvGQPj8AgJiCZSbVbSMygaFxzJIkwVnYYzcche2OnWPL","D_NnbXf9KeVdp3vr8ee8":"j4q5FJu0oqhOkoYlIilxiS9t69FIT4NIsDc","jbyD9wa5jG_SLGIhIrymkphVtaWXmg":"NHw86J3gcrH3oV3VvbGCz35hSmJo0p6J8IK","54EcRes0d42Kvl4PQy5YMeZiBA6y487YS":"NyH_VxIXbjtdBgbPW0avUo27yEhOLMH8l3lp7uuUgy05oZOpwqPHIZe8ymTJdzG","0D_FPgdry0vajFeDGWiLZyntaEbmI30YXopTKTVsl3639":"S7CmCkxlzQP9i7QXAVuXTGjLZHdGsHLrup5gaRd5uBUJdpU","T1hXMxPwU0UUkCR7zJNQGZSrg7Q":"7jhvUcCHyyLxzSGiXZQsH1zg5OzqZCo_DygMGe9mk6PrUsbWuJonDl029hEkXySa77nNltaDYSBOm","8kj7w_Y6k":"_TQFkw9HguuAdZvl4bgObZjzw7LRUsXiUKsptcFzYNrfydyIn027cAVrjFD1UTFmu5IBOr2hCgm396dTvAFAE8LwKI8DnhmxrbBI6XAQKm5QP","yqDIpm0Quk_JwXK4HT0":"frXd2fVsfkJG5IFqlpdakn9RVlBhyHCaa_Mg7TtGx","KjdYK5NM":"tMfvEBFdnl8fyt9raJkFUXFOXhesvqgqRz3Ol5u2GL89mnN3KCSfYkFckVLc1N","Fy9LtOMmMnBYE":{"B":"YagXqoARK6","VP":"qWLZ1MI2","pnvpN":{"U":"Nr"},"rohE0":"cvUmC","Pbhw":"74t7hr","TJ":"3Mk"},"Ak":"LXy64JKyBKF_1IZE_A8L0FkTsYl3","hT3lSx":"fqi3Rmp5sz2AX42CKd2gkh1JK_6vO6","U":{"1":"vlnCLx0","U7K":"v7L","qPa1t8":"cCSjg1zw8","UoC":"yM","_":"YWK1","rb":"fuFb","GPw":"oc","O":"yeYh"},"zlhMkSSf0OWwv5S6d5vqJ_2ShLM4IoH0fxh2LwSOJ9lr8":{"n":"qZ7ZZr","ig":"6bkEukq1MYUFV","Ld":"XHuvT","A":"1RjPg59pSUo4Y"},"MvLalx2Q5QpaGaVX_j0Z9YNm9p1mRcWESaMWNKmhxc1EAjCHaLY5fqB":{"F":"8HydFS","H":"WlloQtFoa","Eq6OH0G":"QmELTDcP0e","kpYZs":"9kHXRZTdkeB9AI"},"ylVhbdvL0sQC5HQrhXAtjL":"JMr1jghNCanFi5ZsvECnjqQiDXZ67YK2FX8tzaT5rI6AjTN_5oeKHShKE0tE_H8mQOYGhFtA9xIqacsleT1TTlLx7I7XO7VH","f4I6Om3FplPn7":"iGL7ogCSbbxTjdl02pYQIX2I","KGNC9Dq8dIx0af5IghQ":{"1":"F","Ax65":{"_":"XjEVVA5so"},"mqgfP":"AOugXkZ5k","vMEh":"oep","wT":"WwWr"},"uEYREjr0h2lo8c2ZKStWh":"r54I4vAJqknpcR5Mj1MYNXyhDvqZvcXy5GVPNvlJRM21Q2NN","wP8u92t3OqycRXA2z8KYZAY3QGpWALnz":"ANRwBIpp2vOmUzMvQKOpqFR1O0B1GixBaXbVZGGpZ5weJKjHcqxBN_qm4yEcrPANPT7Jc_1Vgdg9lyt","tInE1DM7HOAPR9KOeb7KCwX0B1hSb3Eup":"sx4h9csuXOxBIa91pNxPfpreUzrSFNQPuJpgzlTEF9LNXT","HtyX4DbWp":"rnwFENmQJGZhIIdQEroOKpne7XytLMBpRJRhz0BjadvQaRdVrGNSpkjimHf9GNIJtO8xsK196K8AqgK7Ze","TibA":{"B":"ZwA1nRp","dg4G5pS":"sYmsltfVoS","4vKmmp":"p7JHYXZiTZ"},"C5eIXCb1tGc4ctZNoENdy9C":{"W":"n0YZT7","e0d":"QcGD","G":"SdL5d","bal":{"r":"yPwcztqo"},"gBTdB":{"s":"A"},"BmE":{"2":"JDVMkYaQft2IK"}},"sfXF9mlmMBwcRTA5":{"T":"rJz","Vc9E":"lGZ2","R":"gDdTv","H":"zlf7qT8qy8j","tT":"XYhwObr8Wdeg_"},"7aGNrSr0hDzjta":{"6":"y05","M3kf":"AbmDRlOzyM_","BALUSw":"WCDnnB"},"s9a":"eke1zjEeYlfOXqrLZAJxDdxP30BWKgYFz6lAXj_BKzs6bDjGokswyek","PWayAIYXOChAp7D3E":"DQwoBE6h8iLMDJnbVECxTRfpNMUCELDRpLsm_DWt7qdlXa9Bq","2nYg2cLJCKsIhTZ0u9O":"u99dR9KslyZ3mXGUI444GDZO9qLKAyblthr0","J_k1S2ngLraQNSQXzlHVozWCnvc6F9r":{"d":"PlYR","4e6":"KW","V_KGdCH":"Ld5JKD4"},"fTzgtSV0tGqOrGfC6FKqMWziuo32DxZxQ":"NCGC4p6ecPYEOLsoaAUEa8z0KvI3oGWH4A_So4KNgswW5vD6bqXkkB9suVLZsIcndhVSQkvbqQ","k3zOvuzp":"g5wwXBo6HI65v7sOkV8LQJMbLIJp89D","OtR_HeRQTb4llQZavLuycSdb3e9KjM0rwqY":"PvpFSsZkyeUv_kz2Uh_CM9sAOhiil0MmavNo7_sX2m3iKg41oHiFhkYYTFcluw_LAwr1DS_j1SJdIbRVKRt3K","uMVMda":{"2":"pv6xsGTlNqu","d":"G3TPx","BVA":"QQ9Q","Hj":"24f76PPoEpRgXd","K":"TgNFPmvyW","txprr4aiA":"F4MB2ruTU","so1":"cvzmlZlz"},"q":"Rb1pTFLKCxtLm2wLmKsQGB_PM3CZPGjUD6Mu9HOBzcsei_0BN_MUO3nEmfpYZKVhWdIwIps8pHKE","TGeNqVSII8":"EGpAgOkxcbz1MIUjY7kzC","7wn4DVB0":"AydKmTs1JetYLXy9xgsnBLkfQF7SmznQESluJLh3KD","6c":"Q4jO7qHLoSfgEchtQGi0tA9jw1JsE8Y1DjJa","YMpVA6FXxtMjjdXqljBf_6jVqU5":"vus2PPTqYRFRzpbS6iRKYqzp8YZl8wepYzbhZojMI4qsKNour2DSq4Eok3A7O4lRXFxE7AQ2yccQTK","ltDdHTwwO1DjWYfN3cHntniBtJEppYtQIb":"UkFqjgDcqadsah7ncz8uLuH9Rq0sf5kjCwknRFO01uoeSt4JvwvnVr8x6hfKmxPwiv58aA5o74A","wKLoDsvP7gjO":"NiekUYJ7GvGi7JRXoi6uzHFM9YeXDOhpR9y9DAf9LL1grINoBrzgE0Xds8SX54r7ELaB2JE5ulp4t7gxn0f1hnQZLSJLuzqV6GvsLU5YuH","pTod4HDiL":"YM9PQaZEbAnJYbtMT3IhpnGbx0DeC69s","dxXMKM7A7upxVcz":"4ob_DyigaHH7hNuxccjBfL8R_BtevYFzZ18niBCM0JZdaa2q53"}'; + await api.setLedgerInstance({ ledgerName: CLOUD_TO_DEVICE_LEDGER, instance: { data: JSON.parse(jsonData) }, scopeValue: deviceId, org: ORG_ID, auth }); +}); + +test('07_validate_cloud_to_device_sync_large_size', async function() { + // See ledger.cpp +}); diff --git a/user/tests/integration/package-lock.json b/user/tests/integration/package-lock.json index 720071e6da..00559424db 100644 --- a/user/tests/integration/package-lock.json +++ b/user/tests/integration/package-lock.json @@ -1,6 +1,1496 @@ { + "name": "integration", + "lockfileVersion": 2, "requires": true, - "lockfileVersion": 1, + "packages": { + "": { + "dependencies": { + "@particle/device-constants": "^3.1.11", + "binary-version-reader": "^2.2.0", + "chai-exclude": "^2.1.0", + "particle-api-js": "^10.4.2", + "tempy": "^1.0.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@particle/device-constants": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/@particle/device-constants/-/device-constants-3.1.11.tgz", + "integrity": "sha512-/S18G87wWqoOpyWSALYZfHRRNe/jXGYQlbPShcgCGUJoOjO/d0EX8lL0aw2a59cj2uxv5YG7YZURzpyHh5Pylw==", + "engines": { + "node": ">=12.x", + "npm": "8.x" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archiver": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz", + "integrity": "sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==", + "dependencies": { + "archiver-utils": "^2.1.0", + "async": "^3.2.3", + "buffer-crc32": "^0.2.1", + "readable-stream": "^3.6.0", + "readdir-glob": "^1.0.0", + "tar-stream": "^2.2.0", + "zip-stream": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/archiver-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", + "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", + "dependencies": { + "glob": "^7.1.4", + "graceful-fs": "^4.2.0", + "lazystream": "^1.0.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.union": "^4.6.0", + "normalize-path": "^3.0.0", + "readable-stream": "^2.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/archiver-utils/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/binary-version-reader": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-version-reader/-/binary-version-reader-2.2.0.tgz", + "integrity": "sha512-BcGb3yoMHxhctMk2u9+537QpwE3vclhoelF/V4FS0yC3NvfT755+okoWNw2pOZrD2wkdpndkvt3XmdB+kYpH2Q==", + "dependencies": { + "archiver": "^5.3.1", + "buffer-crc32": "^0.2.5", + "tmp-promise": "^3.0.3", + "unzipper": "^0.10.11", + "when": "^3.7.3", + "xtend": "^4.0.2" + }, + "bin": { + "pmod": "bin/pmod.js" + }, + "engines": { + "node": ">=12", + "npm": "8.x" + }, + "peerDependencies": { + "@particle/device-constants": "^3.1.11" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==" + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.0.tgz", + "integrity": "sha512-x9cHNq1uvkCdU+5xTkNh5WtgD4e4yDFCsp9jVc7N7qVeKeftv3gO/ZrviX5d+3ZfxdYnZXZYujjRInu1RogU6A==", + "peer": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chai-exclude": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chai-exclude/-/chai-exclude-2.1.0.tgz", + "integrity": "sha512-IBnm50Mvl3O1YhPpTgbU8MK0Gw7NHcb18WT2TxGdPKOMtdtZVKLHmQwdvOF7mTlHVQStbXuZKFwkevFtbHjpVg==", + "dependencies": { + "fclone": "^1.0.11" + }, + "peerDependencies": { + "chai": ">= 4.0.0 < 5" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "peer": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compress-commons": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", + "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==", + "dependencies": { + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz", + "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "peer": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/del": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", + "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fclone": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz", + "integrity": "sha512-GDqVQezKzRABdeqflsgMr7ktzgF9CyS+p2oe0jJqUY6izSSbhPIQJDpoU4PtGcD7VPM9xh/dVrTu6z1nwgmEGw==" + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.union": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", + "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==" + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "peer": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/particle-api-js": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/particle-api-js/-/particle-api-js-10.4.2.tgz", + "integrity": "sha512-YhG1KdMGrzC8sd8a3UQMM58XTMRTiX5ZzfRf25OJMvym4UIIPMWIFXLa/d1hQIaFqxomZjwqdu8HF7NMVMWK+Q==", + "dependencies": { + "form-data": "^4.0.0", + "node-fetch": "^2.7.0", + "qs": "^6.11.2", + "stream-http": "^3.2.0" + }, + "engines": { + "node": ">=12.x", + "npm": "8.x" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-1.0.1.tgz", + "integrity": "sha512-biM9brNqxSc04Ee71hzFbryD11nX7VPhQQY32AdDmjFvodsRFz/3ufeoTZ6uYkRFfGo188tENcASNs3vTdsM0w==", + "dependencies": { + "del": "^6.0.0", + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/tmp-promise": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", + "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "dependencies": { + "tmp": "^0.2.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "engines": { + "node": "*" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/when": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/when/-/when-3.7.8.tgz", + "integrity": "sha512-5cZ7mecD3eYcMiCH4wtRPA5iFJZ50BJYDfckI5RRpQiktMiYTcn0ccLTZOvcbBume+1304fQztxeNzNS9Gvrnw==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/zip-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", + "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==", + "dependencies": { + "archiver-utils": "^2.1.0", + "compress-commons": "^4.1.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">= 10" + } + } + }, "dependencies": { "@nodelib/fs.scandir": { "version": "2.1.5", @@ -91,11 +1581,22 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==" }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "peer": true + }, "async": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -189,6 +1690,36 @@ "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==" }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==" + }, + "call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "requires": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + } + }, + "chai": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.0.tgz", + "integrity": "sha512-x9cHNq1uvkCdU+5xTkNh5WtgD4e4yDFCsp9jVc7N7qVeKeftv3gO/ZrviX5d+3ZfxdYnZXZYujjRInu1RogU6A==", + "peer": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + } + }, "chai-exclude": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/chai-exclude/-/chai-exclude-2.1.0.tgz", @@ -205,11 +1736,28 @@ "traverse": ">=0.3.0 <0.4" } }, + "check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "peer": true, + "requires": { + "get-func-name": "^2.0.2" + } + }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "compress-commons": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", @@ -250,6 +1798,25 @@ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" }, + "deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "peer": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, "del": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", @@ -265,6 +1832,11 @@ "slash": "^3.0.0" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -338,6 +1910,16 @@ "to-regex-range": "^5.0.1" } }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -369,6 +1951,28 @@ } } }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" + }, + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "peer": true + }, + "get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "requires": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + } + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -403,11 +2007,45 @@ "slash": "^3.0.0" } }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "requires": { + "get-intrinsic": "^1.1.3" + } + }, "graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "requires": { + "get-intrinsic": "^1.2.2" + } + }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "requires": { + "function-bind": "^1.1.2" + } + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -529,6 +2167,15 @@ "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==" }, + "loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "peer": true, + "requires": { + "get-func-name": "^2.0.1" + } + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -543,6 +2190,19 @@ "picomatch": "^2.3.1" } }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -564,11 +2224,24 @@ "minimist": "^1.2.6" } }, + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, + "object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -585,6 +2258,17 @@ "aggregate-error": "^3.0.0" } }, + "particle-api-js": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/particle-api-js/-/particle-api-js-10.4.2.tgz", + "integrity": "sha512-YhG1KdMGrzC8sd8a3UQMM58XTMRTiX5ZzfRf25OJMvym4UIIPMWIFXLa/d1hQIaFqxomZjwqdu8HF7NMVMWK+Q==", + "requires": { + "form-data": "^4.0.0", + "node-fetch": "^2.7.0", + "qs": "^6.11.2", + "stream-http": "^3.2.0" + } + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -595,6 +2279,12 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "peer": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -605,6 +2295,14 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "requires": { + "side-channel": "^1.0.4" + } + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -672,16 +2370,48 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "requires": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" }, + "stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -743,11 +2473,22 @@ "is-number": "^7.0.0" } }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==" }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "peer": true + }, "type-fest": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", @@ -799,6 +2540,20 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "when": { "version": "3.7.8", "resolved": "https://registry.npmjs.org/when/-/when-3.7.8.tgz", diff --git a/user/tests/integration/package.json b/user/tests/integration/package.json index 4533ec1c34..7b441e9a0f 100644 --- a/user/tests/integration/package.json +++ b/user/tests/integration/package.json @@ -4,7 +4,8 @@ "dependencies": { "@particle/device-constants": "^3.1.11", "binary-version-reader": "^2.2.0", - "tempy": "^1.0.1", - "chai-exclude": "^2.1.0" + "chai-exclude": "^2.1.0", + "particle-api-js": "^10.4.2", + "tempy": "^1.0.1" } } diff --git a/user/tests/integration/wiring/ledger b/user/tests/integration/wiring/ledger new file mode 120000 index 0000000000..7ce67b31e0 --- /dev/null +++ b/user/tests/integration/wiring/ledger @@ -0,0 +1 @@ +../../wiring/ledger \ No newline at end of file diff --git a/user/tests/wiring/ledger/application.cpp b/user/tests/wiring/ledger/application.cpp new file mode 100644 index 0000000000..0e6a5d21fe --- /dev/null +++ b/user/tests/wiring/ledger/application.cpp @@ -0,0 +1,14 @@ +#ifndef PARTICLE_TEST_RUNNER + +#include "application.h" +#include "unit-test/unit-test.h" + +SYSTEM_MODE(SEMI_AUTOMATIC); + +#if USE_THREADING +SYSTEM_THREAD(ENABLED); +#endif + +UNIT_TEST_APP(); + +#endif // !defined(PARTICLE_TEST_RUNNER) diff --git a/user/tests/wiring/ledger/ledger.cpp b/user/tests/wiring/ledger/ledger.cpp new file mode 100644 index 0000000000..5f1fbf73b0 --- /dev/null +++ b/user/tests/wiring/ledger/ledger.cpp @@ -0,0 +1,237 @@ +#include + +#include "application.h" +#include "unit-test/unit-test.h" + +#include "scope_guard.h" + +#if Wiring_Ledger + +namespace { + +const auto LEDGER_NAME = "test"; + +struct CountingCallback { + static int instanceCount; + + CountingCallback() { + ++instanceCount; + } + + CountingCallback(const CountingCallback&) { + ++instanceCount; + } + + ~CountingCallback() { + --instanceCount; + } + + void operator()(Ledger ledger) { + } + + CountingCallback& operator=(const CountingCallback&) { + ++instanceCount; + return *this; + } +}; + +int CountingCallback::instanceCount = 0; + +} // namespace + +test(01_remove_all) { + assertEqual(Ledger::removeAll(), 0); +} + +test(02_initial_state) { + auto ledger = Particle.ledger(LEDGER_NAME); + assertTrue(ledger.isValid()); + assertEqual(std::strcmp(ledger.name(), LEDGER_NAME), 0); + assertTrue(ledger.scope() == LedgerScope::UNKNOWN); + assertTrue(ledger.isWritable()); + assertEqual(ledger.lastUpdated(), 0); + assertEqual(ledger.lastSynced(), 0); + assertEqual(ledger.dataSize(), 0); +} + +test(03_replace_data) { + assertEqual(ledger_purge(LEDGER_NAME, nullptr), 0); + { + auto ledger = Particle.ledger(LEDGER_NAME); + LedgerData d = { { "a", 1 }, { "b", 2 }, { "c", 3 } }; + assertEqual(ledger.set(d, Ledger::REPLACE), 0); + assertMore(ledger.dataSize(), 0); + } + { + auto ledger = Particle.ledger(LEDGER_NAME); + auto d = ledger.get(); + assertTrue((d == LedgerData{ { "a", 1 }, { "b", 2 }, { "c", 3 } })); + } +} + +test(04_merge_data) { + assertEqual(ledger_purge(LEDGER_NAME, nullptr), 0); + { + auto ledger = Particle.ledger(LEDGER_NAME); + LedgerData d = { { "d", 4 }, { "f", 6 } }; + assertEqual(ledger.set(d), 0); // Replaces the current data by default + d = { { "e", 5 } }; + assertEqual(ledger.set(d, Ledger::MERGE), 0); + } + { + auto ledger = Particle.ledger(LEDGER_NAME); + auto d = ledger.get(); + assertTrue((d == LedgerData{ { "d", 4 }, { "e", 5 }, { "f", 6 } })); + } +} + +test(05_concurrent_writing) { + assertEqual(ledger_purge(LEDGER_NAME, nullptr), 0); + + // Open two ledger streams for writing + ledger_instance* lr = nullptr; + assertEqual(ledger_get_instance(&lr, LEDGER_NAME, nullptr), 0); + SCOPE_GUARD({ + ledger_release(lr, nullptr); + }); + ledger_stream* ls1 = nullptr; + assertEqual(ledger_open(&ls1, lr, LEDGER_STREAM_MODE_WRITE, nullptr), 0); + NAMED_SCOPE_GUARD(g1, { + ledger_close(ls1, 0, nullptr); + }); + ledger_stream* ls2 = nullptr; + assertEqual(ledger_open(&ls2, lr, LEDGER_STREAM_MODE_WRITE, nullptr), 0); + NAMED_SCOPE_GUARD(g2, { + ledger_close(ls2, 0, nullptr); + }); + + // Write some data to both streams + char cbor1[] = { 0xa3, 0x61, 0x61, 0x01, 0x61, 0x62, 0x02, 0x61, 0x63, 0x03 }; // {"a":1,"b":2,"c":3} + assertEqual(ledger_write(ls1, cbor1, sizeof(cbor1), nullptr), sizeof(cbor1)); + char cbor2[] = { 0xa3, 0x61, 0x64, 0x04, 0x61, 0x65, 0x05, 0x61, 0x66, 0x06 }; // {"d":4,"e":5,"f":6} + assertEqual(ledger_write(ls2, cbor2, sizeof(cbor2), nullptr), sizeof(cbor2)); + + // Close the first stream and check that the data can be read back + g1.dismiss(); + assertEqual(ledger_close(ls1, 0, nullptr), 0); + auto ledger = Particle.ledger(LEDGER_NAME); + auto d = ledger.get(); + assertTrue((d == LedgerData{ { "a", 1 }, { "b", 2 }, { "c", 3 } })); + + // Close the second stream and check that the data can be read back + g2.dismiss(); + assertEqual(ledger_close(ls2, 0, nullptr), 0); + d = ledger.get(); + assertTrue((d == LedgerData{ { "d", 4 }, { "e", 5 }, { "f", 6 } })); +} + +test(06_concurrent_reading_and_writing) { + assertEqual(ledger_purge(LEDGER_NAME, nullptr), 0); + + // Set some initial data + auto ledger = Particle.ledger(LEDGER_NAME); + LedgerData d = { { "a", 1 }, { "b", 2 }, { "c", 3 } }; + assertEqual(ledger.set(d), 0); + + // Open the ledger for reading + ledger_instance* lr = nullptr; + assertEqual(ledger_get_instance(&lr, LEDGER_NAME, nullptr), 0); + SCOPE_GUARD({ + ledger_release(lr, nullptr); + }); + ledger_stream* ls = nullptr; + assertEqual(ledger_open(&ls, lr, LEDGER_STREAM_MODE_READ, nullptr), 0); + SCOPE_GUARD({ + ledger_close(ls, 0, nullptr); + }); + + // Set some new data + d = { { "d", 4 }, { "e", 5 }, { "f", 6 } }; + assertEqual(ledger.set(d), 0); + + // Proceed to read the data + char expectedCbor[] = { 0xa3, 0x61, 0x61, 0x01, 0x61, 0x62, 0x02, 0x61, 0x63, 0x03 }; // {"a":1,"b":2,"c":3} + char cbor[128]; + int n = ledger_read(ls, cbor, sizeof(cbor), nullptr); + + // The reader opened before the modification should see the original data + assertEqual(n, (int)sizeof(expectedCbor)); + assertEqual(std::memcmp(cbor, expectedCbor, sizeof(expectedCbor)), 0); + + // The reader opened after the modification should see the actual data + d = ledger.get(); + assertTrue((d == LedgerData{ { "d", 4 }, { "e", 5 }, { "f", 6 } })); +} + +test(07_multiple_staged_data_files) { + assertEqual(ledger_purge(LEDGER_NAME, nullptr), 0); + + // Open the ledger for reading + ledger_instance* lr = nullptr; + assertEqual(ledger_get_instance(&lr, LEDGER_NAME, nullptr), 0); + SCOPE_GUARD({ + ledger_release(lr, nullptr); + }); + ledger_stream* ls = nullptr; + assertEqual(ledger_open(&ls, lr, LEDGER_STREAM_MODE_READ, nullptr), 0); + NAMED_SCOPE_GUARD(closeStreamGuard, { + ledger_close(ls, 0, nullptr); + }); + + // Write some data to the ledger. This will cause the ledger to create a staged data file as it + // can't overwrite the current one + Ledger ledger = Particle.ledger(LEDGER_NAME); + LedgerData d = { { "a", 1 }, { "b", 2 }, { "c", 3 } }; + assertEqual(ledger.set(d), 0); + + // Open another ledger stream that would read the staged data. This will cause the ledger to + // create a new staged data file rather than overwrite the existing one when Ledger::set() is + // called below + ledger_stream* ls2 = nullptr; + assertEqual(ledger_open(&ls2, lr, LEDGER_STREAM_MODE_READ, nullptr), 0); + NAMED_SCOPE_GUARD(closeStreamGuard2, { + ledger_close(ls2, 0, nullptr); + }); + + // Write some new data to the ledger + d = { { "d", 4 }, { "e", 5 }, { "f", 6 } }; + assertEqual(ledger.set(d), 0); + + // Close the streams + closeStreamGuard.dismiss(); + assertEqual(ledger_close(ls, 0, nullptr), 0); + closeStreamGuard2.dismiss(); + assertEqual(ledger_close(ls2, 0, nullptr), 0); + + // Validate the ledger data + d = ledger.get(); + assertTrue((d == LedgerData{ { "d", 4 }, { "e", 5 }, { "f", 6 } })); +} + +test(08_set_sync_callback) { + auto ledger = Particle.ledger(LEDGER_NAME); + + // Register a functor callback + ledger.onSync(CountingCallback()); + assertEqual(CountingCallback::instanceCount, 1); + + // Register a C callback + Ledger::OnSyncCallback cb = [](Ledger ledger, void* arg) {}; + ledger.onSync(cb); + assertEqual(CountingCallback::instanceCount, 0); + + // Register a functor callback again + ledger.onSync(CountingCallback()); + assertEqual(CountingCallback::instanceCount, 1); + + // Unregister the callback + ledger.onSync(nullptr); + assertEqual(CountingCallback::instanceCount, 0); +} + +test(09_remove) { + // Remove the test ledger files + assertEqual(Ledger::remove(LEDGER_NAME), 0); +} + +#endif // Wiring_Ledger diff --git a/user/tests/wiring/ledger/ledger.spec.js b/user/tests/wiring/ledger/ledger.spec.js new file mode 100644 index 0000000000..5be9b11c4d --- /dev/null +++ b/user/tests/wiring/ledger/ledger.spec.js @@ -0,0 +1,3 @@ +suite('Ledger API'); + +platform('gen3'); diff --git a/user/tests/wiring/ledger/test.mk b/user/tests/wiring/ledger/test.mk new file mode 100644 index 0000000000..62488e25bf --- /dev/null +++ b/user/tests/wiring/ledger/test.mk @@ -0,0 +1,5 @@ +ifeq ("${USE_THREADING}","y") +CFLAGS += -DUSE_THREADING=1 +else +CFLAGS += -DUSE_THREADING=0 +endif diff --git a/wiring/inc/spark_wiring_cloud.h b/wiring/inc/spark_wiring_cloud.h index d812e33d20..333ef1bd4b 100644 --- a/wiring/inc/spark_wiring_cloud.h +++ b/wiring/inc/spark_wiring_cloud.h @@ -34,6 +34,9 @@ #include "spark_wiring_watchdog.h" #include "spark_wiring_async.h" #include "spark_wiring_flags.h" +#include "spark_wiring_platform.h" +#include "spark_wiring_vector.h" +#include "spark_wiring_error.h" #include "spark_wiring_global.h" #include "spark_wiring_network.h" #include "interrupts_hal.h" @@ -65,6 +68,12 @@ struct is_string_literal { static constexpr bool value = std::is_array::value && std::is_same::type, char>::value; }; +namespace particle { + +class Ledger; + +} // namespace particle + class CloudDisconnectOptions { public: CloudDisconnectOptions(); @@ -439,6 +448,36 @@ class CloudClass { */ static int maxFunctionArgumentSize(); +#if Wiring_Ledger + /** + * Get a ledger instance. + * + * @param name Ledger name. + * @return Ledger instance. + */ + static particle::Ledger ledger(const char* name); + + /** + * Remove any ledgers not in the list from the device. + * + * The device must not be connected to the Cloud. The operation will fail if any of the affected + * ledgers is in use. + * + * @note The data is not guaranteed to be removed in an irrecoverable way. + * + * @param ... Ledger names. + * @return 0 on success, otherwise an error code defined by `Error::Type`. + */ + template + static int useLedgers(ArgsT&&... args) { + Vector names; + if (!names.reserve(sizeof...(ArgsT))) { + return particle::Error::NO_MEMORY; + } + return useLedgersImpl(names, std::forward(args)...); + } +#endif // Wiring_Ledger + private: static bool register_function(cloud_function_t fn, void* data, const char* funcKey); @@ -548,6 +587,18 @@ class CloudClass { } return ok; } + +#if Wiring_Ledger + template + static int useLedgersImpl(Vector& names, const char* name, ArgsT&&... args) { + if (!names.append(name)) { + return particle::Error::NO_MEMORY; + } + return useLedgersImpl(names, std::forward(args)...); + } + + static int useLedgersImpl(const Vector& names); +#endif // Wiring_Ledger }; extern CloudClass Spark __attribute__((deprecated("Spark is now Particle."))); diff --git a/wiring/inc/spark_wiring_json.h b/wiring/inc/spark_wiring_json.h index bf8c32466c..d56d712ea1 100644 --- a/wiring/inc/spark_wiring_json.h +++ b/wiring/inc/spark_wiring_json.h @@ -211,6 +211,7 @@ class JSONStreamWriter: public JSONWriter { public: explicit JSONStreamWriter(Print &stream); + size_t bytesWritten() const; Print* stream() const; protected: @@ -218,6 +219,7 @@ class JSONStreamWriter: public JSONWriter { private: Print &strm_; + size_t bytesWritten_; }; class JSONBufferWriter: public JSONWriter { @@ -245,6 +247,12 @@ bool operator!=(const String &str1, const JSONString &str2); } // namespace spark +namespace particle { + +using namespace spark; + +} // namespace particle + // spark::JSONValue inline spark::JSONValue::JSONValue() : t_(nullptr) { @@ -402,7 +410,12 @@ inline void spark::JSONWriter::write(char c) { // spark::JSONStreamWriter inline spark::JSONStreamWriter::JSONStreamWriter(Print &stream) : - strm_(stream) { + strm_(stream), + bytesWritten_(0) { +} + +inline size_t spark::JSONStreamWriter::bytesWritten() const { + return bytesWritten_; } inline Print* spark::JSONStreamWriter::stream() const { @@ -410,7 +423,7 @@ inline Print* spark::JSONStreamWriter::stream() const { } inline void spark::JSONStreamWriter::write(const char *data, size_t size) { - strm_.write((const uint8_t*)data, size); + bytesWritten_ += strm_.write((const uint8_t*)data, size); } // spark::JSONBufferWriter diff --git a/wiring/inc/spark_wiring_ledger.h b/wiring/inc/spark_wiring_ledger.h new file mode 100644 index 0000000000..1ea34900b1 --- /dev/null +++ b/wiring/inc/spark_wiring_ledger.h @@ -0,0 +1,642 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include "spark_wiring_platform.h" + +#if Wiring_Ledger + +#include +#include +#include + +#include "spark_wiring_variant.h" + +#include "system_ledger.h" + +namespace particle { + +class LedgerData; + +/** + * Ledger scope. + */ +enum class LedgerScope { + UNKNOWN = LEDGER_SCOPE_UNKNOWN, ///< Unknown scope. + DEVICE = LEDGER_SCOPE_DEVICE, ///< Device scope. + PRODUCT = LEDGER_SCOPE_PRODUCT, ///< Product scope. + OWNER = LEDGER_SCOPE_OWNER ///< User or organization scope. +}; + +/** + * A ledger. + * + * Use `Particle.ledger()` to create an instance of this class. + */ +class Ledger { +public: + /** + * Mode of operation of the `set()` method. + */ + enum SetMode { + REPLACE, ///< Replace the current ledger data. + MERGE ///< Update some of the entries of the ledger data. + }; + + /** + * A callback invoked when the ledger data has been synchronized with the Cloud. + * + * @param ledger Ledger instance. + * @param arg Callback argument. + */ + typedef void (*OnSyncCallback)(Ledger ledger, void* arg); + + /** + * A callback invoked when the ledger data has been synchronized with the Cloud. + * + * @param ledger Ledger instance. + */ + typedef std::function OnSyncFunction; + + /** + * Default constructor. + * + * Constructs an invalid ledger instance. + */ + Ledger() : + Ledger(nullptr) { + } + + // This constructor is for internal use only + explicit Ledger(ledger_instance* instance, bool addRef = true) : + instance_(instance) { + if (instance_ && addRef) { + ledger_add_ref(instance_, nullptr); + } + } + + /** + * Copy constructor. + * + * @param ledger Ledger instance to copy. + */ + Ledger(const Ledger& ledger) : + Ledger(ledger.instance_) { + } + + /** + * Move constructor. + * + * @param ledger Ledger instance to move from. + */ + Ledger(Ledger&& ledger) : + Ledger() { + swap(*this, ledger); + } + + /** + * Destructor. + */ + ~Ledger() { + if (instance_) { + ledger_release(instance_, nullptr); + } + } + + /** + * Set the ledger data. + * + * @param data New ledger data. + * @param mode Mode of operation. + * @return 0 on success, otherwise an error code defined by `Error::Type`. + */ + int set(const LedgerData& data, SetMode mode = SetMode::REPLACE); + + /** + * Get the ledger data. + * + * @return Ledger data. + */ + LedgerData get() const; + + /** + * Get the time the ledger was last updated, in milliseconds since the Unix epoch. + * + * @return Time the ledger was updated, or 0 if the time is unknown. + */ + int64_t lastUpdated() const; + + /** + * Get the time the ledger was last synchronized with the Cloud, in milliseconds since the Unix epoch. + * + * @return Time the ledger was synchronized, or 0 if the ledger has never been synchronized. + */ + int64_t lastSynced() const; + + /** + * Get the size of the ledger data in bytes. + * + * @return Data size. + */ + size_t dataSize() const; + + /** + * Get the ledger name. + * + * @return Ledger name. + */ + const char* name() const; + + /** + * Get the ledger scope. + * + * @return Ledger scope. + */ + LedgerScope scope() const; + + /** + * Check if the ledger data can be modified by the application. + * + * @return `true` if the ledger data can be modified, otherwise `false`. + */ + bool isWritable() const; + + /** + * Check if the ledger instance is valid. + * + * @return `true` if the instance is valid, otherwise `false`. + */ + bool isValid() const { + return instance_; + } + + /** + * Set a callback to be invoked when the ledger data has been synchronized with the Cloud. + * + * @note Setting a callback will keep an instance of this ledger around until the callback is + * cleared, even if the original instance is no longer referenced in the application code. + * + * The callback can be cleared by calling `onSync()` with a `nullptr`. + * + * @param callback Callback. + * @param arg Argument to pass to the callback. + * @return 0 on success, otherwise an error code defined by `Error::Type`. + */ + int onSync(OnSyncCallback callback, void* arg = nullptr); + + /** + * Set a callback to be invoked when the ledger data has been synchronized with the Cloud. + * + * @note Setting a callback will keep an instance of this ledger around until the callback is + * cleared, even if the original instance is no longer referenced in the application code. + * + * The callback can be cleared by calling `onSync()` with a `nullptr`. + * + * @param callback Callback. + * @return 0 on success, otherwise an error code defined by `Error::Type`. + */ + int onSync(OnSyncFunction callback); + + /** + * Assignment operator. + * + * @param ledger Ledger instance to assign from. + * @return This ledger instance. + */ + Ledger& operator=(Ledger ledger) { + swap(*this, ledger); + return *this; + } + + /** + * Comparison operators. + * + * Two ledger instances are considered equal if they were created for the same ledger: + * ``` + * Ledger myLedger = Particle.ledger("my-ledger"); + * + * void onSyncCallback(Ledger ledger) { + * if (ledger == myLedger) { + * Log.info("my-ledger synchronized") + * } + * } + * ``` + */ + ///@{ + bool operator==(const Ledger& ledger) const { + return instance_ == ledger.instance_; + } + + bool operator!=(const Ledger& ledger) const { + return instance_ != ledger.instance_; + } + ///@} + + /** + * Get the names of the ledgers stored on the device. + * + * @param[out] names Ledger names. + * @return 0 on success, otherwise an error code defined by `Error::Type`. + */ + static int getNames(Vector& names); + + /** + * Remove any data associated with a ledger from the device. + * + * The device must not be connected to the Cloud. The operation will fail if the ledger with the + * given name is in use. + * + * @note The data is not guaranteed to be removed in an irrecoverable way. + * + * @param name Ledger name. + * @return 0 on success, otherwise an error code defined by `Error::Type`. + */ + static int remove(const char* name); + + /** + * Remove any ledger data from the device. + * + * The device must not be connected to the Cloud. The operation will fail if any of the ledgers + * is in use. + * + * @note The data is not guaranteed to be removed in an irrecoverable way. + * + * @return 0 on success, otherwise an error code defined by `Error::Type`. + */ + static int removeAll(); + + friend void swap(Ledger& ledger1, Ledger& ledger2) { + using std::swap; + swap(ledger1.instance_, ledger2.instance_); + } + +private: + ledger_instance* instance_; +}; + +/** + * Ledger data. + * + * This class provides a subset of methods of the `Variant` class that are relevant to map operations. + */ +class LedgerData { +public: + /** + * Data entry. + * + * A data entry is an `std::pair` where `first` is the entry name (a `String`) and `second` is + * the entry value (a `Variant`). + */ + using Entry = VariantMap::Entry; + + /** + * Construct empty ledger data. + */ + LedgerData() : + v_(VariantMap()) { + } + + /** + * Construct ledger data from a `Variant`. + * + * If the `Variant` is not a map, empty ledger data is constructed. + * + * @param var `Variant` value. + */ + LedgerData(Variant var) { + if (var.isMap()) { + v_ = std::move(var); + } else { + v_ = var.toMap(); + } + } + + /** + * Construct ledger data from an initializer list. + * + * Example usage: + * ``` + * LedgerData data = { { "key1", "value1" }, { "key2", 2 } }; + * ``` + * + * @param entries Entries. + */ + LedgerData(std::initializer_list entries) : + v_(VariantMap(entries)) { + } + + /** + * Copy constructor. + * + * @param data Ledger data to copy. + */ + LedgerData(const LedgerData& data) : + v_(data.v_) { + } + + /** + * Move constructor. + * + * @param data Ledger data to move from. + */ + LedgerData(LedgerData&& data) : + LedgerData() { + swap(*this, data); + } + + ///@{ + /** + * Set the value of an entry. + * + * @param name Entry name. + * @param val Entry value. + * @return `true` if the entry value was set, or `false` on a memory allocation error. + */ + bool set(const char* name, Variant val) { + return v_.set(name, std::move(val)); + } + + bool set(const String& name, Variant val) { + return v_.set(name, std::move(val)); + } + + bool set(String&& name, Variant val) { + return v_.set(std::move(name), std::move(val)); + } + ///@} + + ///@{ + /** + * Remove an entry. + * + * @param name Entry name. + * @return `true` if the entry was removed, otherwise `false`. + */ + bool remove(const char* name) { + return v_.remove(name); + } + + bool remove(const String& name) { + return v_.remove(name); + } + ///@} + + ///@{ + /** + * Get the value of an entry. + * + * This method is inefficient for complex value types, such as `String`, as it returns a copy of + * the value. Use `operator[]` to get a reference to the value. + * + * A null `Variant` is returned if the entry doesn't exist. + * + * @param name Entry name. + * @return Entry value. + */ + Variant get(const char* name) const { + return v_.get(name); + } + + Variant get(const String& name) const { + return v_.get(name); + } + ///@} + + ///@{ + /** + * Check if an entry with the given name exists. + * + * @param name Entry name. + * @return `true` if the entry exists, otherwise `false`. + */ + bool has(const char* name) const { + return v_.has(name); + } + + bool has(const String& name) const { + return v_.has(name); + } + ///@} + + /** + * Get all entries of the ledger data. + * + * Example usage: + * ``` + * LedgerData data = myLedger.get(); + * for (const LedgerData::Entry& entry: data.entries()) { + * const String& name = entry.first; + * const Variant& value = entry.second; + * Log.info("%s: %s", name.c_str(), value.toString().c_str()); + * } + * ``` + * + * @return Entries. + */ + const Vector& entries() const { + return variantMap().entries(); + } + + /** + * Get the number of entries stored in the ledger data. + * + * @return Number of entries. + */ + int size() const { + return v_.size(); + } + + /** + * Get the number of entries that can be stored without reallocating memory. + * + * @return Number of entries. + */ + int capacity() const { + return variantMap().capacity(); + } + + /** + * Reserve memory for the specified number of entries. + * + * @return `true` on success, or `false` on a memory allocation error. + */ + bool reserve(int capacity) { + return variantMap().reserve(capacity); + } + + /** + * Reduce the capacity of the ledger data to its actual size. + * + * @return `true` on success, or `false` on a memory allocation error. + */ + bool trimToSize() { + return variantMap().trimToSize(); + } + + /** + * Check if the ledger data is empty. + * + * @return `true` if the data is empty, otherwise `false`. + */ + bool isEmpty() const { + return v_.isEmpty(); + } + + /** + * Serialize the ledger data as JSON. + * + * @return JSON document. + */ + String toJSON() const { + return v_.toJSON(); + } + + ///@{ + /** + * Get a reference to the `VariantMap` containing the entries of the ledger data. + * + * @return Reference to the `VariantMap`. + */ + VariantMap& variantMap() { + return v_.value(); + } + + const VariantMap& variantMap() const { + return v_.value(); + } + ///@} + + ///@{ + /** + * Get a reference to the underlying `Variant`. + * + * @return Reference to the `Variant`. + */ + Variant& variant() { + return v_; + } + + const Variant& variant() const { + return v_; + } + ///@} + + ///@{ + /** + * Get a reference to the entry value. + * + * The entry is created if it doesn't exist. + * + * @note The device will panic if it fails to allocate memory for the new entry. Use `set()` + * or the methods provided by `VariantMap` if you need more control over how memory allocation + * errors are handled. + * + * @param name Entry name. + * @return Entry value. + */ + Variant& operator[](const char* name) { + return v_[name]; + } + + Variant& operator[](const String& name) { + return v_[name]; + } + + Variant& operator[](String&& name) { + return v_[std::move(name)]; + } + ///@} + + /** + * Assignment operator. + * + * @param data Ledger data to assign from. + * @return This ledger data. + */ + LedgerData& operator=(LedgerData data) { + swap(*this, data); + return *this; + } + + /** + * Comparison operators. + * + * Two instances of ledger data are equal if they contain equal sets of entries. + */ + ///@{ + bool operator==(const LedgerData& data) const { + return v_ == data.v_; + } + + bool operator!=(const LedgerData& data) const { + return v_ != data.v_; + } + + bool operator==(const Variant& var) const { + return v_ == var; + } + + bool operator!=(const Variant& var) const { + return v_ != var; + } + ///@} + + /** + * Parse ledger data from JSON. + * + * If the root element of the JSON document is not an object, empty ledger data is returned. + * + * @param json JSON document. + * @return Ledger data. + */ + static LedgerData fromJSON(const char* json) { + return Variant::fromJSON(json); + } + + /** + * Convert a JSON value to ledger data. + * + * If the JSON value is not an object, empty ledger data is returned. + * + * @param Val JSON value. + * @return Ledger data. + */ + static LedgerData fromJSON(const JSONValue& val) { + return Variant::fromJSON(val); + } + + friend void swap(LedgerData& data1, LedgerData& data2) { + using std::swap; // For ADL + swap(data1.v_, data2.v_); + } + +private: + Variant v_; +}; + +inline bool operator==(const Variant& var, const LedgerData& data) { + return data == var; +} + +inline bool operator!=(const Variant& var, const LedgerData& data) { + return data != var; +} + +} // namespace particle + +#endif // Wiring_Ledger diff --git a/wiring/inc/spark_wiring_map.h b/wiring/inc/spark_wiring_map.h new file mode 100644 index 0000000000..6cd90e2e44 --- /dev/null +++ b/wiring/inc/spark_wiring_map.h @@ -0,0 +1,501 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include +#include +#include + +#include "spark_wiring_vector.h" + +#include "debug.h" + +namespace particle { + +/** + * An ordered associative container with unique keys. + * + * Internally, `Map` stores its entries in a dynamically allocated array. The entries are sorted by + * key and binary search is used for lookup. + * + * @tparam KeyT Key type. + * @tparam ValueT Value type. + * @tparam CompareT Comparator type. + */ +template> +class Map { +public: + /** + * Key type. + */ + typedef KeyT Key; + + /** + * Value type. + */ + typedef ValueT Value; + + /** + * Comparator type. + */ + typedef CompareT Compare; + + /** + * Entry type. + */ + typedef std::pair Entry; + + /** + * Iterator type. + */ + typedef typename Vector::Iterator Iterator; + + /** + * Constant interator type. + */ + typedef typename Vector::ConstIterator ConstIterator; + + /** + * Construct an empty map. + */ + Map() = default; + + /** + * Construct a map from an initializer list. + * + * @param entries Entries. + */ + Map(std::initializer_list entries) : + Map() { + Map map; + if (!map.reserve(entries.size())) { + return; + } + for (auto& e: entries) { + if (!map.set(e.first, e.second)) { + return; + } + } + swap(*this, map); + } + + /** + * Copy constructor. + * + * @param map Map to copy. + */ + Map(const Map& map) : + entries_(map.entries_), + cmp_(map.cmp_) { + } + + /** + * Move constructor. + * + * @param map Map to move from. + */ + Map(Map&& map) : + Map() { + swap(*this, map); + } + + ///@{ + /** + * Add or update an entry. + * + * @param key Key. + * @param val Value. + * @return `true` if the entry was added or updated, or `false` on a memory allocation error. + */ + template + bool set(const T& key, ValueT val) { + auto r = insert(key, std::move(val)); + if (r.first == entries_.end()) { + return false; + } + return true; + } + + bool set(KeyT&& key, ValueT val) { + auto r = insert(std::move(key), std::move(val)); + if (r.first == entries_.end()) { + return false; + } + return true; + } + ///@} + + /** + * Get the value of an entry. + * + * This method is inefficient for complex value types as it returns a copy of the entry value. + * Use `operator[]` to get a reference to the entry value, or `find()` to get an iterator + * pointing to the value. + * + * A default-constructed value is returned if an entry with the given key cannot be found. + * + * @param key Key. + * @return Value. + */ + template + ValueT get(const T& key) const { + auto it = find(key); + if (it == entries_.end()) { + return ValueT(); + } + return it->second; + } + + /** + * Get the value of an entry. + * + * This method is inefficient for complex value types as it returns a copy of the entry value. + * Use `operator[]` to get a reference to the entry value, or `find()` to get an iterator + * pointing to the value. + * + * @param key Key. + * @param defaultVal Value to return if an entry with the given key cannot be found. + * @return Value. + */ + template + ValueT get(const T& key, const ValueT& defaultVal) const { + auto it = find(key); + if (it == entries_.end()) { + return defaultVal; + } + return it->second; + } + + /** + * Remove an entry. + * + * @param key Key. + * @return `true` if the entry was removed, otherwise `false`. + */ + template + bool remove(const T& key) { + auto it = find(key); + if (it == entries_.end()) { + return false; + } + entries_.erase(it); + return true; + } + + /** + * Check if the map contains an entry. + * + * @param key Key. + * @return `true` if an entry with the given key is found, otherwise `false`. + */ + template + bool has(const T& key) const { + auto it = find(key); + if (it == entries_.end()) { + return false; + } + return true; + } + + /** + * Get all entries of the map. + * + * @return Entries. + */ + const Vector& entries() const { + return entries_; + } + + /** + * Get the number of entries in the map. + * + * @return Number of entries. + */ + int size() const { + return entries_.size(); + } + + /** + * Check if the map is empty. + * + * @return `true` if the map is empty, otherwise `false`. + */ + bool isEmpty() const { + return entries_.isEmpty(); + } + + /** + * Reserve memory for the specified number of entries. + * + * @param count Number of entries. + * @return `true` on success, or `false` on a memory allocation error. + */ + bool reserve(int count) { + return entries_.reserve(count); + } + + /** + * Get the number of entries that can be stored without reallocating memory. + * + * @return Number of entries. + */ + int capacity() const { + return entries_.capacity(); + } + + /** + * Reduce the capacity of the map to its actual size. + * + * @return `true` on success, or `false` on a memory allocation error. + */ + bool trimToSize() { + return entries_.trimToSize(); + } + + /** + * Remove all entries. + */ + void clear() { + entries_.clear(); + } + + ///@{ + /** + * Get an iterator pointing to the first entry of the map. + * + * @return Iterator. + */ + Iterator begin() { + return entries_.begin(); + } + + ConstIterator begin() const { + return entries_.begin(); + } + ///@} + + ///@{ + /** + * Get an iterator pointing to the entry following the last entry of the map. + * + * @return Iterator. + */ + Iterator end() { + return entries_.end(); + } + + ConstIterator end() const { + return entries_.end(); + } + ///@} + + ///@{ + /** + * Find an entry. + * + * If an entry with the given key cannot be found, an iterator pointing to the entry following + * the last entry of the map is returned. + * + * @param key Key. + * @return Iterator pointing to the entry. + */ + template + Iterator find(const T& key) { + auto it = lowerBound(key); + if (it != entries_.end() && cmp_(key, it->first)) { + return entries_.end(); + } + return it; + } + + template + ConstIterator find(const T& key) const { + auto it = lowerBound(key); + if (it != entries_.end() && cmp_(key, it->first)) { + return entries_.end(); + } + return it; + } + ///@} + + ///@{ + /** + * Add or update an entry. + * + * On a memory allocation error, an iterator pointing to the entry following the last entry of + * the map is returned. + * + * @param key Key. + * @param val Value. + * @return `std::pair` where `first` is an iterator pointing to the entry, and `second` is set + * to `true` if the entry was inserted, or `false` if it was updated. + */ + template + std::pair insert(const T& key, ValueT val) { + auto it = lowerBound(key); + if (it != entries_.end() && !cmp_(key, it->first)) { + it->second = std::move(val); + return std::make_pair(it, false); + } + it = entries_.insert(it, std::make_pair(KeyT(key), std::move(val))); + if (it == entries_.end()) { + return std::make_pair(it, false); + } + return std::make_pair(it, true); + } + + std::pair insert(KeyT&& key, ValueT val) { + auto it = lowerBound(key); + if (it != entries_.end() && !cmp_(key, it->first)) { + it->second = std::move(val); + return std::make_pair(it, false); + } + it = entries_.insert(it, std::make_pair(std::move(key), std::move(val))); + if (it == entries_.end()) { + return std::make_pair(it, false); + } + return std::make_pair(it, true); + } + ///@} + + /** + * Remove an entry. + * + * @param pos Iterator pointing to the entry to be removed. + * @return Iterator pointing to the entry following the removed entry. + */ + Iterator erase(ConstIterator pos) { + return entries_.erase(pos); + } + + ///@{ + /** + * Get an iterator pointing to first entry of the map whose key compares not less than the + * provided key. + * + * @param key Key. + * @return Iterator. + */ + template + Iterator lowerBound(const T& key) { + return std::lower_bound(entries_.begin(), entries_.end(), key, [this](const Entry& entry, const T& key) { + return this->cmp_(entry.first, key); + }); + } + + template + ConstIterator lowerBound(const T& key) const { + return std::lower_bound(entries_.begin(), entries_.end(), key, [this](const Entry& entry, const T& key) { + return this->cmp_(entry.first, key); + }); + } + ///@} + + ///@{ + /** + * Get an iterator pointing to first entry of the map whose key compares greater than the + * provided key. + * + * @param key Key. + * @return Iterator. + */ + template + Iterator upperBound(const T& key) { + return std::upper_bound(entries_.begin(), entries_.end(), key, [this](const Entry& entry, const T& key) { + return this->cmp_(entry.first, key); + }); + } + + template + ConstIterator upperBound(const T& key) const { + return std::upper_bound(entries_.begin(), entries_.end(), key, [this](const Entry& entry, const T& key) { + return this->cmp_(entry.first, key); + }); + } + ///@} + + ///@{ + /** + * Get a reference to the value of an entry. + * + * The entry is created if it doesn't exist. + * + * @note The device will panic if it fails to allocate memory for the new entry. Use `set()` or + * `insert()` if you need more control over how memory allocation errors are handled. + * + * @param key Key. + * @return Value. + */ + template + ValueT& operator[](const T& key) { + auto it = lowerBound(key); + if (it == entries_.end() || cmp_(key, it->first)) { + it = entries_.insert(it, std::make_pair(KeyT(key), ValueT())); + SPARK_ASSERT(it != entries_.end()); + } + return it->second; + } + + ValueT& operator[](KeyT&& key) { + auto it = lowerBound(key); + if (it == entries_.end() || cmp_(key, it->first)) { + it = entries_.insert(it, std::make_pair(std::move(key), ValueT())); + SPARK_ASSERT(it != entries_.end()); + } + return it->second; + } + ///@} + + /** + * Assignment operator. + * + * @param map Map to assign from. + * @return This map. + */ + Map& operator=(Map map) { + swap(*this, map); + return *this; + } + + /** + * Comparison operators. + * + * Two maps are equal if they contain equal sets of entries. + */ + ///@{ + bool operator==(const Map& map) const { + return entries_ == map.entries_; + } + + bool operator!=(const Map& map) const { + return entries_ != map.entries_; + } + ///@} + + friend void swap(Map& map1, Map& map2) { + using std::swap; // For ADL + swap(map1.entries_, map2.entries_); + swap(map1.cmp_, map2.cmp_); + } + +private: + Vector entries_; + CompareT cmp_; +}; + +} // namespace particle diff --git a/wiring/inc/spark_wiring_platform.h b/wiring/inc/spark_wiring_platform.h index f044fbc86e..199672cd9d 100644 --- a/wiring/inc/spark_wiring_platform.h +++ b/wiring/inc/spark_wiring_platform.h @@ -72,6 +72,10 @@ #define Wiring_Watchdog 1 #endif // HAL_PLATFORM_HW_WATCHDOG +#if HAL_PLATFORM_LEDGER +#define Wiring_Ledger 1 +#endif + #ifndef Wiring_SPI1 #define Wiring_SPI1 0 #endif @@ -166,5 +170,9 @@ #define Wiring_Watchdog 0 #endif // Wiring_Watchdog +#ifndef Wiring_Ledger +#define Wiring_Ledger 0 +#endif + #endif /* SPARK_WIRING_PLATFORM_H */ diff --git a/wiring/inc/spark_wiring_print.h b/wiring/inc/spark_wiring_print.h index 7884a3af4b..adc2748f14 100644 --- a/wiring/inc/spark_wiring_print.h +++ b/wiring/inc/spark_wiring_print.h @@ -33,10 +33,10 @@ #include // for uint8_t #include "system_tick_hal.h" -#include "spark_wiring_string.h" #include "spark_wiring_printable.h" #include "spark_wiring_fixed_point.h" #include +#include const unsigned char DEC = 10; const unsigned char HEX = 16; @@ -46,6 +46,12 @@ const unsigned char BIN = 2; class String; class __FlashStringHelper; +namespace particle { + +class Variant; + +} // namespace particle + class Print { private: @@ -62,6 +68,8 @@ class Print #ifndef PARTICLE_WIRING_PRINT_NO_FLOAT size_t printFloat(double, uint8_t); #endif // PARTICLE_WIRING_PRINT_NO_FLOAT + size_t printVariant(const particle::Variant& var); + protected: void setWriteError(int err = 1) { write_error = err; } @@ -69,7 +77,7 @@ class Print Print() : write_error(0) {} virtual ~Print() {} - int getWriteError() { return write_error; } + int getWriteError() const { return write_error; } void clearWriteError() { setWriteError(0); } virtual size_t write(uint8_t) = 0; @@ -89,6 +97,12 @@ class Print size_t print(double, int = 2); #endif // PARTICLE_WIRING_PRINT_NO_FLOAT + // Prevent implicit constructors of Variant from affecting overload resolution + template, int> = 0> + size_t print(const T& var) { + return printVariant(var); + } + size_t print(const Printable&); size_t print(const __FlashStringHelper*); @@ -105,6 +119,14 @@ class Print size_t println(float, int = 2); size_t println(double, int = 2); #endif // PARTICLE_WIRING_PRINT_NO_FLOAT + + template, int> = 0> + size_t println(const T& var) { + size_t n = printVariant(var); + n += println(); + return n; + } + size_t println(const Printable&); size_t println(void); size_t println(const __FlashStringHelper*); @@ -130,9 +152,29 @@ class Print size_t vprintf(bool newline, const char* format, va_list args) __attribute__ ((format(printf, 3, 0))); }; +namespace particle { + +class OutputStringStream: public Print { +public: + explicit OutputStringStream(String& str) : + s_(str) { + } + + size_t write(uint8_t b) override { + return write(&b, 1); + } + + size_t write(const uint8_t* data, size_t size) override; + +private: + String& s_; +}; + +} // namespace particle + template ::value && (std::is_integral::value || std::is_convertible::value || std::is_convertible::value), int>> -size_t Print::print(T n, int base) +inline size_t Print::print(T n, int base) { if (base == 0) { return write(n); @@ -157,4 +199,5 @@ size_t Print::print(T n, int base) return printNumber(val, base) + t; } } + #endif diff --git a/wiring/inc/spark_wiring_stream.h b/wiring/inc/spark_wiring_stream.h index 2547aa758e..c821e1cdd1 100644 --- a/wiring/inc/spark_wiring_stream.h +++ b/wiring/inc/spark_wiring_stream.h @@ -79,7 +79,7 @@ class Stream : public Print float parseFloat(); // float version of parseInt - size_t readBytes( char *buffer, size_t length); // read chars from stream into buffer + virtual size_t readBytes( char *buffer, size_t length); // read chars from stream into buffer // terminates if length characters have been read or timeout (see setTimeout) // returns the number of characters placed in the buffer (0 means no valid data found) diff --git a/wiring/inc/spark_wiring_string.h b/wiring/inc/spark_wiring_string.h index d614d43084..91602aa550 100644 --- a/wiring/inc/spark_wiring_string.h +++ b/wiring/inc/spark_wiring_string.h @@ -94,6 +94,10 @@ class String unsigned char reserve(unsigned int size); inline unsigned int length(void) const {return len;} + unsigned int capacity() const { + return capacity_; + } + // creates a copy of the assigned value. if the value is null or // invalid, or if the memory allocation fails, the string will be // marked as invalid ("if (s)" will be false). @@ -105,7 +109,7 @@ class String String & operator = (StringSumHelper &&rval); #endif - operator const char*() const { return c_str(); } + operator const char*() const { return c_str(); } // concatenate (works w/ built-in types) @@ -114,6 +118,7 @@ class String // concatenation is considered unsucessful. unsigned char concat(const String &str); unsigned char concat(const char *cstr); + unsigned char concat(const char *cstr, unsigned int length); unsigned char concat(const __FlashStringHelper * str); unsigned char concat(char c); unsigned char concat(unsigned char c); @@ -211,14 +216,13 @@ class String protected: char *buffer; // the actual char array - unsigned int capacity; // the array length minus one (for the '\0') + unsigned int capacity_; // the array length minus one (for the '\0') unsigned int len; // the String length (not counting the '\0') unsigned char flags; // unused, for future features protected: void init(void); void invalidate(void); unsigned char changeBuffer(unsigned int maxStrLen); - unsigned char concat(const char *cstr, unsigned int length); // copy and move String & copy(const char *cstr, unsigned int length); @@ -227,9 +231,6 @@ class String #ifdef __GXX_EXPERIMENTAL_CXX0X__ void move(String &rhs); #endif - - friend class StringPrintableHelper; - }; class StringSumHelper : public String diff --git a/wiring/inc/spark_wiring_variant.h b/wiring/inc/spark_wiring_variant.h new file mode 100644 index 0000000000..cfaa30c325 --- /dev/null +++ b/wiring/inc/spark_wiring_variant.h @@ -0,0 +1,1058 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include + +#include "spark_wiring_string.h" +#include "spark_wiring_vector.h" +#include "spark_wiring_map.h" + +#include "debug.h" + +class Stream; +class Print; + +namespace spark { + +class JSONValue; + +} // namespace spark + +namespace particle { + +using spark::JSONValue; + +class Variant; + +/** + * An array of `Variant` values. + */ +typedef Vector VariantArray; + +/** + * A map of named `Variant` values. + */ +typedef Map VariantMap; + +namespace detail { + +template +struct IsComparableWithVariant { + // TODO: This is not ideal as we'd like Variant to be comparable with any type as long as it's + // comparable with one of the Variant's alternative types + static constexpr bool value = std::is_same_v || std::is_arithmetic_v || std::is_same_v || + std::is_same_v || std::is_same_v || std::is_same_v; +}; + +} // namespace detail + +/** + * A class acting as a union for certain common data types. + */ +class Variant { +public: + /** + * Supported alternative types. + */ + enum Type { + NULL_, ///< Null type (`std::monostate`). + BOOL, ///< `bool`. + INT, ///< `int`. + UINT, ///< `unsigned`. + INT64, ///< `int64_t`. + UINT64, ///< `uint64_t`. + DOUBLE, ///< `double`. + STRING, ///< `String`. + ARRAY, ///< `VariantArray`. + MAP ///< `VariantMap`. + }; + + /** + * Construct a null variant. + */ + Variant() = default; + + ///@{ + /** + * Construct a variant with a value. + * + * @param val Value. + */ + Variant(const std::monostate& val) : + Variant() { + } + + Variant(bool val) : + v_(val) { + } + + Variant(int val) : + v_(val) { + } + + Variant(unsigned val) : + v_(val) { + } +#ifdef __LP64__ + Variant(long val) : + v_(static_cast(val)) { + } + + Variant(unsigned long val) : + v_(static_cast(val)) { + } +#else + Variant(long val) : + v_(static_cast(val)) { + } + + Variant(unsigned long val) : + v_(static_cast(val)) { + } +#endif + Variant(long long val) : + v_(val) { + } + + Variant(unsigned long long val) : + v_(val) { + } + + Variant(double val) : + v_(val) { + } + + Variant(const char* val) : + v_(String(val)) { + } + + Variant(String val) : + v_(std::move(val)) { + } + + Variant(VariantArray val) : + v_(std::move(val)) { + } + + Variant(VariantMap val) : + v_(std::move(val)) { + } + ///@} + + /** + * Copy constructor. + * + * @param var Variant to copy. + */ + Variant(const Variant& var) : + v_(var.v_) { + } + + /** + * Move constructor. + * + * @param var Variant to move from. + */ + Variant(Variant&& var) : + Variant() { + swap(*this, var); + } + + /** + * Get the type of the stored value. + * + * @return Value type. + */ + Type type() const { + return static_cast(v_.index()); + } + + ///@{ + /** + * Check if the stored value is of the specified type. + * + * @return `true` if the value is of the specified type, otherwise `false`. + */ + bool isNull() const { + return is(); + } + + bool isBool() const { + return is(); + } + + bool isInt() const { + return is(); + } + + bool isUInt() const { + return is(); + } + + bool isInt64() const { + return is(); + } + + bool isUInt64() const { + return is(); + } + + bool isDouble() const { + return is(); + } + + bool isNumber() const { + auto t = type(); + return t >= Type::INT && t <= Type::DOUBLE; + } + + bool isString() const { + return is(); + } + + bool isArray() const { + return is(); + } + + bool isMap() const { + return is(); + } + ///@} + + /** + * Check if the stored value is of the specified type. + * + * The type `T` must be one of the supported alternative types (see the `Type` enum). + * + * @tparam T Value type. + * @return `true` if the value is of the specified type, otherwise `false`. + */ + template + bool is() const { + static_assert(IsAlternativeType::value, "The type specified is not one of the alternative types of Variant"); + return std::holds_alternative(v_); + } + + ///@{ + /** + * Convert the stored value to a value of the specified type. + * + * See the description of the method `to(bool&)` for details on how the conversion is performed. + * + * @return Converted value. + */ + bool toBool() const { + return to(); + } + + int toInt() const { + return to(); + } + + unsigned toUInt() const { + return to(); + } + + int64_t toInt64() const { + return to(); + } + + uint64_t toUInt64() const { + return to(); + } + + double toDouble() const { + return to(); + } + + String toString() const { + return to(); + } + + VariantArray toArray() const { + return to(); + } + + VariantMap toMap() const { + return to(); + } + ///@} + + ///@{ + /** + * Convert the stored value to a value of the specified type. + * + * See the description of the method `to(bool&)` for details on how the conversion is performed. + * + * @param[out] ok Set to `true` if a conversion is defined for the current and target types, + * or to `false` otherwise. + * @return Converted value. + */ + bool toBool(bool& ok) const { + return to(ok); + } + + int toInt(bool& ok) const { + return to(ok); + } + + unsigned toUInt(bool& ok) const { + return to(ok); + } + + int64_t toInt64(bool& ok) const { + return to(ok); + } + + uint64_t toUInt64(bool& ok) const { + return to(ok); + } + + double toDouble(bool& ok) const { + return to(ok); + } + + String toString(bool& ok) const { + return to(ok); + } + + VariantArray toArray(bool& ok) const { + return to(ok); + } + + VariantMap toMap(bool& ok) const { + return to(ok); + } + ///@} + + /** + * Convert the stored value to a value of the specified type. + * + * See the description of the method `to(bool&)` for details on how the conversion is performed. + * + * @tparam T Target type. + * @return Converted value. + */ + template + T to() const { + return std::visit(ConvertToVisitor(), v_); + } + + /** + * Convert the stored value to a value of the specified type. + * + * The conversion is performed as follows: + * + * - If the type of the stored value is the same as the target type, a copy of the stored value + * is returned. To avoid copying, one of the methods that return a reference to the stored + * value can be used instead. + * + * - If the current and target types are numeric (or boolean), the conversion is performed as + * if `static_cast` is used. It is not checked whether the current value is within the range + * of the target type. + * + * - If one of the types is `String` and the other type is numeric (or boolean), a conversion + * to or from string is performed. When converted to a string, boolean values are represented + * as "true" or "false". + * + * - For the null, array and map types, only a trivial conversion to the same type is defined. + * + * - If no conversion is defined for the current and target types, a default-constructed value + * of the target type is returned. + * + * @param[out] ok Set to `true` if a conversion is defined for the current and target types, + * or to `false` otherwise. + * @return Converted value. + */ + template + T to(bool& ok) const { + ConvertToVisitor vis; + T val = std::visit(vis, v_); + ok = vis.ok; + return val; + } + + ///@{ + /** + * Convert the stored value to a value of the specified type in place and get a reference to + * the stored value. + * + * @return Reference to the stored value. + */ + bool& asBool() { + return as(); + } + + int& asInt() { + return as(); + } + + unsigned& asUInt() { + return as(); + } + + int64_t& asInt64() { + return as(); + } + + uint64_t& asUInt64() { + return as(); + } + + double& asDouble() { + return as(); + } + + String& asString() { + return as(); + } + + VariantArray& asArray() { + return as(); + } + + VariantMap& asMap() { + return as(); + } + ///@} + + /** + * Convert the stored value to a value of the specified type in place and get a reference to + * the stored value. + * + * The type `T` must be one of the supported alternative types (see the `Type` enum). + * + * @tparam T Target type. + * @return Reference to the stored value. + */ + template + T& as() { + static_assert(IsAlternativeType::value, "The type specified is not one of the alternative types of Variant"); + if (!is()) { + v_ = to(); + } + return value(); + } + + ///@{ + /** + * Get a reference to the stored value. + * + * The type `T` must be one of the supported alternative types (see the `Type` enum). If the + * stored value has a different type, the behavior is undefined. + * + * @tparam T Target type. + * @return Reference to the stored value. + */ + template + T& value() { + static_assert(IsAlternativeType::value, "The type specified is not one of the alternative types of Variant"); + return std::get(v_); + } + + template + const T& value() const { + static_assert(IsAlternativeType::value, "The type specified is not one of the alternative types of Variant"); + return std::get(v_); + } + ///@} + + /** + * Array operations. + * + * These methods are provided for convenience and behave similarly to the respective methods of + * `VariantArray`. + */ + ///@{ + + /** + * Append an element to the array. + * + * If the stored value is not an array, it is converted to an array in place prior to the + * operation. + * + * @param val Element value. + * @return `true` if the element was added, or `false` on a memory allocation error. + */ + bool append(Variant val); + + /** + * Prepend an element to the array. + * + * If the stored value is not an array, it is converted to an array in place prior to the + * operation. + * + * @param val Element value. + * @return `true` if the element was added, or `false` on a memory allocation error. + */ + bool prepend(Variant val); + + /** + * Insert an element into the array. + * + * If the stored value is not an array, it is converted to an array in place prior to the + * operation. + * + * @param index Index at which to insert the element. + * @param val Element value. + * @return `true` if the element was added, or `false` on a memory allocation error. + */ + bool insertAt(int index, Variant val); + + /** + * Remove an element from the array. + * + * The method has no effect if the stored value is not an array. + * + * @param index Element index. + */ + void removeAt(int index); + + /** + * Get an element of the array. + * + * This method is inefficient if the element value has a complex type, such as `String`, as it + * returns a copy of the value. Use `operator[](int)` to get a reference to the element value. + * + * A null variant is returned if the stored value is not an array. + * + * @param index Element index. + * @return Element value. + */ + Variant at(int index) const; + ///@} + + /** + * Map operations. + * + * These methods are provided for convenience and behave similarly to the respective methods of + * `VariantMap`. + */ + ///@{ + + ///@{ + /** + * Add or update an element in the map. + * + * If the stored value is not a map, it is converted to a map in place prior to the operation. + * + * @param key Element key. + * @param val Element value. + * @return `true` if the element was added or updated, or `false` on a memory allocation error. + */ + bool set(const char* key, Variant val); + bool set(const String& key, Variant val); + bool set(String&& key, Variant val); + ///@} + + ///@{ + /** + * Remove an element from the map. + * + * If the stored value is not a map, the method has no effect and returns `false`. + * + * @param key Element key. + * @return `true` if the element was removed, otherwise `false`. + */ + bool remove(const char* key); + bool remove(const String& key); + ///@} + + ///@{ + /** + * Get the value of an element in the map. + * + * This method is inefficient if the element value has a complex type, such as `String`, as it + * returns a copy of the value. Use `operator[](const char*)` to get a reference to the element + * value. + * + * A null variant is returned if the stored value is not a map or if an element with the given + * key cannot be found. + * + * @param key Element key. + * @return Element value. + */ + Variant get(const char* key) const; + Variant get(const String& key) const; + ///@} + + ///@{ + /** + * Check if the map contains an element with the given key. + * + * `false` is returned if the stored value is not a map. + * + * @param key Element key. + * @return `true` if an element with the given key is found, otherwise `false`. + */ + bool has(const char* key) const; + bool has(const String& key) const; + ///@} + ///@} + + /** + * Get the size of the stored value. + * + * Depending on the type of the stored value: + * + * - If `String`, the length of the string is returned. + * + * - If `VariantArray` or `VariantMap`, the number of elements stored in the respective + * container is returned. + * + * - In all other cases 0 is returned. + * + * @return Size of the stored value. + */ + int size() const; + + /** + * Check if the stored value is empty. + * + * Depending on the type of the stored value: + * + * - If `String`, `true` is returned if the string is empty. + * + * - If `VariantArray` or `VariantMap`, `true` is returned if the respective container is empty. + * + * - If the value is null, `true` is returned. + * + * - In all other cases `false` is returned. + * + * @return `true` if the stored value is empty, otherwise `false`. + */ + bool isEmpty() const; + + /** + * Convert the variant to JSON. + * + * @return JSON document. + */ + String toJSON() const; + + ///@{ + /** + * Get a reference to an element in the array. + * + * @note The device will panic if the stored value is not an array. + * + * @param index Element index. + * @return Reference to the element value. + */ + Variant& operator[](int index) { + SPARK_ASSERT(isArray()); + return value().at(index); + } + + const Variant& operator[](int index) const { + SPARK_ASSERT(isArray()); + return value().at(index); + } + ///@} + + ///@{ + /** + * Get a reference to the value of an element in the map. + * + * If the stored value is not a map, it is converted to a map in place prior to the operation. + * + * If an element with the given key cannot be found, it is created and added to the map. + * + * @note The device will panic if it fails to allocate memory for the new element. Use `set()` + * or the methods provided by `VariantMap` if you need more control over how memory allocation + * errors are handled. + * + * @param key Element key. + * @return Reference to the element value. + */ + Variant& operator[](const char* key) { + return asMap().operator[](key); + } + + Variant& operator[](const String& key) { + return asMap().operator[](key); + } + + Variant& operator[](String&& key) { + return asMap().operator[](std::move(key)); + } + ///@} + + /** + * Assignment operator. + * + * @param var Variant to assign from. + * @return This variant. + */ + Variant& operator=(Variant var) { + swap(*this, var); + return *this; + } + + /** + * Comparison operators. + * + * Two variants are considered equal if their stored values are equal. Standard C++ type + * promotion rules are used when comparing numeric values. + */ + ///@{ + bool operator==(const Variant& var) const { + return std::visit(AreEqualVisitor(), v_, var.v_); + } + + bool operator!=(const Variant& var) const { + return !operator==(var); + } + + template::value, int> = 0> + bool operator==(const T& val) const { + return std::visit(IsEqualToVisitor(val), v_); + } + + template::value, int> = 0> + bool operator!=(const T& val) const { + return !operator==(val); + } + ///@} + + /** + * Parse a variant from JSON. + * + * @param json JSON document. + * @return Variant. + */ + static Variant fromJSON(const char* json); + + /** + * Convert a JSON value to a variant. + * + * @param val JSON value. + * @return Variant. + */ + static Variant fromJSON(const JSONValue& val); + + friend void swap(Variant& var1, Variant& var2) { + using std::swap; // For ADL + swap(var1.v_, var2.v_); + } + +private: + template + struct ConvertToVisitor { + bool ok = false; + + template + TargetT operator()(const SourceT& val) { + return TargetT(); + } + }; + + // Compares a Variant with a value + template + struct IsEqualToVisitor { + const FirstT& first; + + explicit IsEqualToVisitor(const FirstT& first) : + first(first) { + } + + template + bool operator()(const SecondT& second) const { + return areEqual(first, second); + } + }; + + // Compares two Variants + struct AreEqualVisitor { + template + bool operator()(const FirstT& first, const SecondT& second) const { + return areEqual(first, second); + } + }; + + template + struct IsAlternativeType { + static constexpr bool value = false; + }; + + typedef std::variant VariantType; + + VariantType v_; + + template + static bool areEqual(const FirstT& first, const SecondT& second) { + // TODO: Use std::equality_comparable_with (requires C++20) + if constexpr (std::is_same_v || + (std::is_arithmetic_v && std::is_arithmetic_v) || + (std::is_same_v && std::is_same_v) || + (std::is_same_v && std::is_same_v)) { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wsign-compare" + return first == second; +#pragma GCC diagnostic pop + } else { + return false; + } + } + + template + static bool ensureCapacity(ContainerT& cont, int count) { + int newSize = cont.size() + count; + if (cont.capacity() >= newSize) { + return true; + } + return cont.reserve(std::max(newSize, cont.capacity() * 3 / 2)); + } +}; + +namespace detail { + +// As of GCC 10, std::to_chars and std::from_chars don't support floating point types. Note that the +// substitution functions below behave differently from the standard ones. They're tailored for the +// needs of the Variant class and are only defined so that we can quickly drop them when +// API is fully supported by the current version of GCC +#if !defined(__cpp_lib_to_chars) || defined(UNIT_TEST) + +std::to_chars_result to_chars(char* first, char* last, double value); +std::from_chars_result from_chars(const char* first, const char* last, double& value); + +#endif // !defined(__cpp_lib_to_chars) || defined(UNIT_TEST) + +template +inline std::to_chars_result to_chars(char* first, char* last, const T& value) { + return std::to_chars(first, last, value); +} + +template +inline std::from_chars_result from_chars(const char* first, const char* last, T& value) { + return std::from_chars(first, last, value); +} + +} // namespace detail + +template<> +struct Variant::ConvertToVisitor { + bool ok = false; + + std::monostate operator()(const std::monostate&) { + ok = true; + return std::monostate(); + } + + template + std::monostate operator()(const SourceT& val) { + return std::monostate(); + } +}; + +template<> +class Variant::ConvertToVisitor { +public: + bool ok = false; + + bool operator()(const String& val) { + if (val == "true") { + ok = true; + return true; + } + if (val == "false") { + ok = true; + return false; + } + return false; + } + + template + bool operator()(const SourceT& val) { + if constexpr (std::is_arithmetic_v) { + ok = true; + return static_cast(val); + } else { // std::monostate, VariantArray, VariantMap + return false; + } + } +}; + +template +struct Variant::ConvertToVisitor>> { + bool ok = false; + + TargetT operator()(const String& val) { + TargetT v; + auto end = val.c_str() + val.length(); + auto r = detail::from_chars(val.c_str(), end, v); + if (r.ec != std::errc() || r.ptr != end) { + return TargetT(); + } + ok = true; + return v; + } + + template + TargetT operator()(const SourceT& val) { + if constexpr (std::is_arithmetic_v) { + ok = true; + return static_cast(val); + } else { // std::monostate, VariantArray, VariantMap + return TargetT(); + } + } +}; + +template<> +struct Variant::ConvertToVisitor { + bool ok = false; + + String operator()(bool val) { + ok = true; + return val ? "true" : "false"; + } + + String operator()(const String& val) { + ok = true; + return val; + } + + template + String operator()(const SourceT& val) { + if constexpr (std::is_arithmetic_v) { + char buf[32]; // Large enough for all relevant types + auto r = detail::to_chars(buf, buf + sizeof(buf), val); + SPARK_ASSERT(r.ec == std::errc()); + ok = true; + return String(buf, r.ptr - buf); + } else { // std::monostate, VariantArray, VariantMap + return String(); + } + } +}; + +template<> +struct Variant::ConvertToVisitor { + bool ok = false; + + VariantArray operator()(const VariantArray& val) { + ok = true; + return val; + } + + template + VariantArray operator()(const SourceT& val) { + return VariantArray(); + } +}; + +template<> +struct Variant::ConvertToVisitor { + bool ok = false; + + VariantMap operator()(const VariantMap& val) { + ok = true; + return val; + } + + template + VariantMap operator()(const SourceT& val) const { + return VariantMap(); + } +}; + +template<> +struct Variant::IsAlternativeType { + static const bool value = true; +}; + +template<> +struct Variant::IsAlternativeType { + static const bool value = true; +}; + +template<> +struct Variant::IsAlternativeType { + static const bool value = true; +}; + +template<> +struct Variant::IsAlternativeType { + static const bool value = true; +}; + +template<> +struct Variant::IsAlternativeType { + static const bool value = true; +}; + +template<> +struct Variant::IsAlternativeType { + static const bool value = true; +}; + +template<> +struct Variant::IsAlternativeType { + static const bool value = true; +}; + +template<> +struct Variant::IsAlternativeType { + static const bool value = true; +}; + +template<> +struct Variant::IsAlternativeType { + static const bool value = true; +}; + +template<> +struct Variant::IsAlternativeType { + static const bool value = true; +}; + +template::value, int> = 0> +inline bool operator==(const T& val, const Variant& var) { + return var == val; +} + +template::value, int> = 0> +inline bool operator!=(const T& val, const Variant& var) { + return var != val; +} + +/** + * Encode a variant to CBOR. + * + * @param var Variant. + * @param stream Output stream. + * @return 0 on success, otherwise an error code defined by `Error::Type`. + */ +int encodeToCBOR(const Variant& var, Print& stream); + +/** + * Decode a variant from CBOR. + * + * @param[out] var Variant. + * @param stream Input stream. + * @return 0 on success, otherwise an error code defined by `Error::Type`. + */ +int decodeFromCBOR(Variant& var, Stream& stream); + +} // namespace particle diff --git a/wiring/inc/spark_wiring_vector.h b/wiring/inc/spark_wiring_vector.h index 4e7819deb3..7d17a90f10 100644 --- a/wiring/inc/spark_wiring_vector.h +++ b/wiring/inc/spark_wiring_vector.h @@ -51,6 +51,8 @@ class Vector { public: typedef T ValueType; typedef AllocatorT AllocatorType; + typedef T* Iterator; + typedef const T* ConstIterator; Vector(); explicit Vector(int n); @@ -114,10 +116,13 @@ class Vector { T* data(); const T* data() const; - T* begin(); - const T* begin() const; - T* end(); - const T* end() const; + Iterator begin(); + ConstIterator begin() const; + Iterator end(); + ConstIterator end() const; + + Iterator insert(ConstIterator pos, T value); + Iterator erase(ConstIterator pos); T& operator[](int i); const T& operator[](int i) const; @@ -607,25 +612,41 @@ inline const T* spark::Vector::data() const { } template -inline T* spark::Vector::begin() { +inline typename spark::Vector::Iterator spark::Vector::begin() { return data_; } template -const T* spark::Vector::begin() const { +inline typename spark::Vector::ConstIterator spark::Vector::begin() const { return data_; } template -T* spark::Vector::end() { +inline typename spark::Vector::Iterator spark::Vector::end() { return data_ + size_; } template -const T* spark::Vector::end() const { +inline typename spark::Vector::ConstIterator spark::Vector::end() const { return data_ + size_; } +template +inline typename spark::Vector::Iterator spark::Vector::insert(ConstIterator pos, T value) { + int i = pos - data_; + if (!insert(i, std::move(value))) { + return data_ + size_; + } + return data_ + i; +} + +template +inline typename spark::Vector::Iterator spark::Vector::erase(ConstIterator pos) { + int i = pos - data_; + removeAt(i); + return data_ + i; +} + template inline T& spark::Vector::operator[](int i) { return data_[i]; diff --git a/wiring/src/build.mk b/wiring/src/build.mk index e890a4fa43..39a5d9440d 100644 --- a/wiring/src/build.mk +++ b/wiring/src/build.mk @@ -15,8 +15,6 @@ CPPSRC += $(call target_files,src/,*.cpp) # ASM source files included in this build. ASRC += -CPPFLAGS += -std=gnu++14 - BUILTINS_EXCLUDE = malloc free realloc CFLAGS += $(addprefix -fno-builtin-,$(BUILTINS_EXCLUDE)) diff --git a/wiring/src/spark_wiring_cloud.cpp b/wiring/src/spark_wiring_cloud.cpp index 08c1a6b67a..aea33fa5bb 100644 --- a/wiring/src/spark_wiring_cloud.cpp +++ b/wiring/src/spark_wiring_cloud.cpp @@ -1,5 +1,7 @@ #include "spark_wiring_cloud.h" +#include "spark_wiring_ledger.h" + #include #include "system_cloud.h" #include "check.h" @@ -124,3 +126,39 @@ int CloudClass::maxFunctionArgumentSize() { CHECK(spark_get_connection_property(SPARK_CLOUD_MAX_FUNCTION_ARGUMENT_SIZE, &size, &n, nullptr /* reserved */)); return size; } + +#if Wiring_Ledger + +Ledger CloudClass::ledger(const char* name) { + ledger_instance* instance = nullptr; + int r = ledger_get_instance(&instance, name, nullptr); + if (r < 0) { + LOG(ERROR, "ledger_get_instance() failed: %d", r); + return Ledger(); + } + return Ledger(instance, false /* addRef */); +} + +int CloudClass::useLedgersImpl(const Vector& usedNames) { + Vector allNames; + CHECK(Ledger::getNames(allNames)); + int result = 0; + for (auto& name: allNames) { + bool found = false; + for (auto usedName: usedNames) { + if (name == usedName) { + found = true; + break; + } + } + if (!found) { + int r = Ledger::remove(name); + if (r < 0 && result >= 0) { + result = r; + } + } + } + return result; +} + +#endif // Wiring_Ledger diff --git a/wiring/src/spark_wiring_json.cpp b/wiring/src/spark_wiring_json.cpp index 17dc898640..624fe6ec1e 100644 --- a/wiring/src/spark_wiring_json.cpp +++ b/wiring/src/spark_wiring_json.cpp @@ -18,11 +18,13 @@ #include "spark_wiring_json.h" #include +#include #include #include #include #include +#include namespace { @@ -63,6 +65,16 @@ bool hexToInt(const char *s, size_t size, uint32_t *val) { return true; } +double toFinite(double val) { + if (std::isnan(val)) { + return 0; + } + if (std::isinf(val)) { + return (val < 0) ? std::numeric_limits::lowest() : std::numeric_limits::max(); + } + return val; +} + } // namespace // spark::detail::JSONData @@ -547,14 +559,14 @@ spark::JSONWriter& spark::JSONWriter::value(unsigned long long val) { spark::JSONWriter& spark::JSONWriter::value(double val, int precision) { writeSeparator(); - printf("%.*lf", precision, val); + printf("%.*lf", precision, toFinite(val)); // NaN and infinite values are not permitted by the spec state_ = NEXT; return *this; } spark::JSONWriter& spark::JSONWriter::value(double val) { writeSeparator(); - printf("%g", val); + printf("%g", toFinite(val)); state_ = NEXT; return *this; } diff --git a/wiring/src/spark_wiring_ledger.cpp b/wiring/src/spark_wiring_ledger.cpp new file mode 100644 index 0000000000..053298f545 --- /dev/null +++ b/wiring/src/spark_wiring_ledger.cpp @@ -0,0 +1,434 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#include "spark_wiring_platform.h" + +#if Wiring_Ledger + +#include +#include + +#include "spark_wiring_ledger.h" + +#include "spark_wiring_stream.h" +#include "spark_wiring_error.h" + +#include "system_task.h" + +#include "scope_guard.h" +#include "check.h" + +namespace particle { + +namespace { + +class LedgerStream: public Stream { +public: + explicit LedgerStream(ledger_instance* ledger) : + ledger_(ledger), + stream_(nullptr), + bytesRead_(0), + bytesWritten_(0) { + } + + ~LedgerStream() { + close(LEDGER_STREAM_CLOSE_DISCARD); + } + + int read() override { + uint8_t b; + size_t n = readBytes((char*)&b, 1); + if (n != 1) { + return -1; + } + return b; + } + + size_t readBytes(char* data, size_t size) override { + if (!stream_ || error() < 0) { + return 0; + } + int r = ledger_read(stream_, data, size, nullptr); + if (r < 0) { + // Suppress the error message if the ledger data is empty + if (r != Error::END_OF_STREAM || bytesRead_ > 0) { + LOG(ERROR, "ledger_read() failed: %d", r); + } + setError(r); + return 0; + } + bytesRead_ += r; + return r; + } + + size_t write(uint8_t b) override { + return write(&b, 1); + } + + size_t write(const uint8_t* data, size_t size) override { + if (!stream_ || error() < 0) { + return 0; + } + int r = ledger_write(stream_, (const char*)data, size, nullptr); + if (r < 0) { + LOG(ERROR, "ledger_write() failed: %d", r); + setError(r); + return 0; + } + bytesWritten_ += r; + return r; + } + + int available() override { + return -1; // Not supported + } + + int peek() override { + return -1; // Not supported + } + + void flush() override { + } + + int open(int mode) { + close(LEDGER_STREAM_CLOSE_DISCARD); + int r = ledger_open(&stream_, ledger_, mode, nullptr); + if (r < 0) { + LOG(ERROR, "ledger_open() failed: %d", r); + return r; + } + return 0; + } + + int close(int flags = 0) { + if (!stream_) { + return 0; + } + int r = ledger_close(stream_, flags, nullptr); + if (r < 0) { + LOG(ERROR, "ledger_close() failed: %d", r); + } + clearWriteError(); + stream_ = nullptr; + bytesRead_ = 0; + bytesWritten_ = 0; + return r; + } + + size_t bytesRead() const { + return bytesRead_; + } + + size_t bytesWritten() const { + return bytesWritten_; + } + + int error() const { + return getWriteError(); // TODO: Rename to error() or add an alias + } + +private: + ledger_instance* ledger_; + ledger_stream* stream_; + size_t bytesRead_; + size_t bytesWritten_; + + void setError(int error) { + setWriteError(error); // TODO: Rename to setError() or add an alias + } +}; + +struct LedgerAppData { + Ledger::OnSyncFunction onSync; +}; + +void destroyLedgerAppData(void* appData) { + delete static_cast(appData); +} + +LedgerAppData* getLedgerAppData(ledger_instance* ledger) { + auto appData = static_cast(ledger_get_app_data(ledger, nullptr)); + if (!appData) { + ledger_lock(ledger, nullptr); + SCOPE_GUARD({ + ledger_unlock(ledger, nullptr); + }); + appData = static_cast(ledger_get_app_data(ledger, nullptr)); + if (!appData) { + appData = new(std::nothrow) LedgerAppData(); + if (appData) { + ledger_set_app_data(ledger, appData, destroyLedgerAppData, nullptr); + } + } + } + return appData; +} + +// Callback wrapper executed in the application thread +void syncCallbackApp(void* data) { + auto ledger = static_cast(data); + SCOPE_GUARD({ + ledger_release(ledger, nullptr); + }); + auto appData = getLedgerAppData(ledger); + if (appData && appData->onSync) { + appData->onSync(Ledger(ledger)); + } +} + +// Callback wrapper executed in the system thread +void syncCallbackSystem(ledger_instance* ledger, void* appData) { + // Dispatch the callback to the application thread + ledger_add_ref(ledger, nullptr); + int r = application_thread_invoke(syncCallbackApp, ledger, nullptr); + if (r != 0) { // FIXME: application_thread_invoke() doesn't really handle errors as of now + ledger_release(ledger, nullptr); + } +} + +// TODO: Generalize this code when there are more callbacks supported +int setSyncCallback(ledger_instance* ledger, Ledger::OnSyncFunction callback) { + ledger_lock(ledger, nullptr); + SCOPE_GUARD({ + ledger_unlock(ledger, nullptr); + }); + auto appData = getLedgerAppData(ledger); + if (!appData) { + return Error::NO_MEMORY; + } + bool hadCallback = !!appData->onSync; + appData->onSync = std::move(callback); + if (appData->onSync) { + if (!hadCallback) { + ledger_callbacks callbacks = {}; + callbacks.version = LEDGER_API_VERSION; + callbacks.sync = syncCallbackSystem; + ledger_set_callbacks(ledger, &callbacks, nullptr); + // Keep the ledger instance around until the callback is cleared + ledger_add_ref(ledger, nullptr); + } + } else if (hadCallback) { + ledger_set_callbacks(ledger, nullptr, nullptr); // Clear the callback + ledger_release(ledger, nullptr); + } + return 0; +} + +int getLedgerInfo(ledger_instance* ledger, ledger_info& info) { + info.version = LEDGER_API_VERSION; + int r = ledger_get_info(ledger, &info, nullptr); + if (r < 0) { + LOG(ERROR, "ledger_get_info() failed: %d", r); + return r; + } + return 0; +} + +int setLedgerData(ledger_instance* ledger, const LedgerData& data) { + LedgerStream stream(ledger); + CHECK(stream.open(LEDGER_STREAM_MODE_WRITE)); + int r = encodeToCBOR(data.variant(), stream); + if (r < 0) { + // encodeToCBOR() can't forward stream errors + int err = stream.error(); + if (err < 0) { + r = err; + } + LOG(ERROR, "Failed to encode ledger data: %d", r); + return r; + } + CHECK(stream.close()); // Flush the data + return 0; +} + +int getLedgerData(ledger_instance* ledger, LedgerData& data) { + LedgerStream stream(ledger); + CHECK(stream.open(LEDGER_STREAM_MODE_READ)); + Variant v; + int r = decodeFromCBOR(v, stream); + if (r < 0) { + // decodeFromCBOR() can't forward stream errors + int err = stream.error(); + if (err < 0) { + r = err; + } + if (r == Error::END_OF_STREAM && !stream.bytesRead()) { + // Treat empty data as an empty map + data = LedgerData(); + return 0; + } + LOG(ERROR, "Failed to decode ledger data: %d", r); + return r; + } + if (!v.isMap()) { + LOG(ERROR, "Unexpected type of ledger data"); + return Error::BAD_DATA; + } + data = std::move(v); + return 0; +} + +} // namespace + +int Ledger::set(const LedgerData& data, SetMode mode) { + if (!isValid()) { + return Error::INVALID_STATE; + } + if (mode == Ledger::REPLACE) { + CHECK(setLedgerData(instance_, data)); + } else { + LedgerData d; + CHECK(getLedgerData(instance_, d)); + for (auto& e: data.variantMap()) { + if (!d.set(e.first, e.second)) { + return Error::NO_MEMORY; + } + } + CHECK(setLedgerData(instance_, d)); + } + return 0; +} + +LedgerData Ledger::get() const { + if (!isValid()) { + return LedgerData(); + } + LedgerData data; + if (getLedgerData(instance_, data) < 0) { + return LedgerData(); + } + return data; +} + +int64_t Ledger::lastUpdated() const { + ledger_info info = {}; + if (!isValid() || getLedgerInfo(instance_, info) < 0) { + return 0; + } + return info.last_updated; +} + +int64_t Ledger::lastSynced() const { + ledger_info info = {}; + if (!isValid() || getLedgerInfo(instance_, info) < 0) { + return 0; + } + return info.last_synced; +} + +size_t Ledger::dataSize() const { + ledger_info info = {}; + if (!isValid() || getLedgerInfo(instance_, info) < 0) { + return 0; + } + return info.data_size; +} + +const char* Ledger::name() const { + ledger_info info = {}; + if (!isValid() || getLedgerInfo(instance_, info) < 0) { + return ""; + } + return info.name; +} + +LedgerScope Ledger::scope() const { + ledger_info info = {}; + if (!isValid() || getLedgerInfo(instance_, info) < 0) { + return LedgerScope::UNKNOWN; + } + return static_cast(info.scope); +} + +bool Ledger::isWritable() const { + ledger_info info = {}; + if (!isValid() || getLedgerInfo(instance_, info) < 0) { + return false; + } + // It's allowed to write to a ledger while its sync direction is unknown + return info.sync_direction == LEDGER_SYNC_DIRECTION_DEVICE_TO_CLOUD || + info.sync_direction == LEDGER_SYNC_DIRECTION_UNKNOWN; +} + +int Ledger::onSync(OnSyncCallback callback, void* arg) { + if (!isValid()) { + return Error::INVALID_STATE; + } + if (!callback) { + return onSync(Ledger::OnSyncFunction()); + } + return onSync([callback, arg](Ledger ledger) { + callback(std::move(ledger), arg); + }); +} + +int Ledger::onSync(OnSyncFunction callback) { + if (!isValid()) { + return Error::INVALID_STATE; + } + return setSyncCallback(instance_, std::move(callback)); +} + +int Ledger::getNames(Vector& namesVec) { + char** names = nullptr; + size_t count = 0; + int r = ledger_get_names(&names, &count, nullptr); + if (r < 0) { + LOG(ERROR, "ledger_get_names() failed: %d", r); + return r; + } + SCOPE_GUARD({ + for (size_t i = 0; i < count; ++i) { + std::free(names[i]); + } + std::free(names); + }); + namesVec.clear(); + if (!namesVec.reserve(count)) { + return Error::NO_MEMORY; + } + for (size_t i = 0; i < count; ++i) { + String name(names[i]); + if (!name.length()) { + return Error::NO_MEMORY; + } + namesVec.append(std::move(name)); + } + return 0; +} + +int Ledger::remove(const char* name) { + int r = ledger_purge(name, nullptr); + if (r < 0) { + LOG(ERROR, "ledger_purge() failed: %d", r); + return r; + } + return 0; +} + +int Ledger::removeAll() { + int r = ledger_purge_all(nullptr); + if (r < 0) { + LOG(ERROR, "ledger_purge_all() failed: %d", r); + return r; + } + return 0; +} + +} // namespace particle + +#endif // Wiring_Ledger diff --git a/wiring/src/spark_wiring_print.cpp b/wiring/src/spark_wiring_print.cpp index fe47a07738..779db026bf 100644 --- a/wiring/src/spark_wiring_print.cpp +++ b/wiring/src/spark_wiring_print.cpp @@ -28,8 +28,72 @@ #include #include #include "spark_wiring_print.h" +#include "spark_wiring_json.h" +#include "spark_wiring_variant.h" #include "spark_wiring_string.h" -#include "spark_wiring_stream.h" +#include "spark_wiring_error.h" + +using namespace particle; + +namespace { + +void writeVariant(const Variant& var, JSONStreamWriter& writer) { + switch (var.type()) { + case Variant::NULL_: { + writer.nullValue(); + break; + } + case Variant::BOOL: { + writer.value(var.value()); + break; + } + case Variant::INT: { + writer.value(var.value()); + break; + } + case Variant::UINT: { + writer.value(var.value()); + break; + } + case Variant::INT64: { + writer.value(var.value()); + break; + } + case Variant::UINT64: { + writer.value(var.value()); + break; + } + case Variant::DOUBLE: { + writer.value(var.value()); + break; + } + case Variant::STRING: { + writer.value(var.value()); + break; + } + case Variant::ARRAY: { + writer.beginArray(); + for (auto& v: var.value()) { + writeVariant(v, writer); + } + writer.endArray(); + break; + } + case Variant::MAP: { + writer.beginObject(); + for (auto& e: var.value().entries()) { + writer.name(e.first); + writeVariant(e.second, writer); + } + writer.endObject(); + break; + } + default: + break; + } +} + +} // namespace // Public Methods ////////////////////////////////////////////////////////////// @@ -217,6 +281,12 @@ size_t Print::printFloat(double number, uint8_t digits) } #endif // PARTICLE_WIRING_PRINT_NO_FLOAT +size_t Print::printVariant(const Variant& var) { + JSONStreamWriter writer(*this); + writeVariant(var, writer); + return writer.bytesWritten(); +} + size_t Print::vprintf(bool newline, const char* format, va_list args) { const int bufsize = 20; @@ -242,3 +312,19 @@ size_t Print::vprintf(bool newline, const char* format, va_list args) return n; } +namespace particle { + +size_t OutputStringStream::write(const uint8_t* data, size_t size) { + if (getWriteError()) { + return 0; + } + size_t newSize = s_.length() + size; + if (s_.capacity() < newSize && !s_.reserve(std::max({ newSize, s_.capacity() * 3 / 2, 20 }))) { + setWriteError(Error::NO_MEMORY); + return 0; + } + s_.concat((const char*)data, size); + return size; +} + +} // namespace particle diff --git a/wiring/src/spark_wiring_string.cpp b/wiring/src/spark_wiring_string.cpp index 48bfe957cd..2265a78ecc 100644 --- a/wiring/src/spark_wiring_string.cpp +++ b/wiring/src/spark_wiring_string.cpp @@ -34,6 +34,8 @@ #include #include "string_convert.h" +using namespace particle; + //These are very crude implementations - will refine later //------------------------------------------------------------------------------------------ @@ -203,7 +205,7 @@ String::~String() inline void String::init(void) { buffer = nullptr; - capacity = 0; + capacity_ = 0; len = 0; flags = 0; } @@ -214,12 +216,12 @@ void String::invalidate(void) free(buffer); } buffer = nullptr; - capacity = len = 0; + capacity_ = len = 0; } unsigned char String::reserve(unsigned int size) { - if (buffer && capacity >= size) { + if (buffer && capacity_ >= size) { return 1; } if (changeBuffer(size)) { @@ -236,7 +238,7 @@ unsigned char String::changeBuffer(unsigned int maxStrLen) char *newbuffer = (char *)realloc(buffer, maxStrLen + 1); if (newbuffer) { buffer = newbuffer; - capacity = maxStrLen; + capacity_ = maxStrLen; return 1; } return 0; @@ -266,7 +268,7 @@ String & String::copy(const __FlashStringHelper *pstr, unsigned int length) { void String::move(String &rhs) { if (buffer) { - if (capacity >= rhs.len && rhs.buffer) { + if (capacity_ >= rhs.len && rhs.buffer) { strcpy(buffer, rhs.buffer); len = rhs.len; rhs.len = 0; @@ -276,10 +278,10 @@ void String::move(String &rhs) } } buffer = rhs.buffer; - capacity = rhs.capacity; + capacity_ = rhs.capacity_; len = rhs.len; rhs.buffer = nullptr; - rhs.capacity = 0; + rhs.capacity_ = 0; rhs.len = 0; } #endif @@ -363,7 +365,8 @@ unsigned char String::concat(const char *cstr, unsigned int length) if (!reserve(newlen)) { return 0; } - strcpy(buffer + len, cstr); + memcpy(buffer + len, cstr, length); + buffer[newlen] = 0; len = newlen; return 1; } @@ -865,7 +868,7 @@ String& String::replace(const String& find, const String& replace) if (size == len) { return *this; } - if (size > capacity && !changeBuffer(size)) { + if (size > capacity_ && !changeBuffer(size)) { return *this; // XXX: tell user! } int index = len - 1; @@ -878,7 +881,7 @@ String& String::replace(const String& find, const String& replace) index--; } } - return *this; + return *this; } String& String::remove(unsigned int index){ @@ -980,34 +983,11 @@ float String::toFloat(void) const return 0; } -class StringPrintableHelper : public Print -{ - String& s; - -public: - - StringPrintableHelper(String& s_) : s(s_) { - s.reserve(20); - } - - virtual size_t write(const uint8_t *buffer, size_t size) override - { - unsigned len = s.length(); - s.concat((const char*)buffer, size); - return s.length()-len; - } - - virtual size_t write(uint8_t c) override - { - return s.concat((char)c); - } -}; - String::String(const Printable& printable) { init(); - StringPrintableHelper help(*this); - printable.printTo(help); + OutputStringStream stream(*this); + printable.printTo(stream); } String String::format(const char* fmt, ...) diff --git a/wiring/src/spark_wiring_variant.cpp b/wiring/src/spark_wiring_variant.cpp new file mode 100644 index 0000000000..4e1737a853 --- /dev/null +++ b/wiring/src/spark_wiring_variant.cpp @@ -0,0 +1,823 @@ +/* + * Copyright (c) 2023 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "spark_wiring_variant.h" + +#include "spark_wiring_json.h" +#include "spark_wiring_stream.h" +#include "spark_wiring_error.h" + +#include "endian_util.h" +#include "check.h" + +namespace particle { + +namespace { + +class DecodingStream { +public: + explicit DecodingStream(Stream& stream) : + stream_(stream) { + } + + int readUint8(uint8_t& val) { + CHECK(read((char*)&val, sizeof(val))); + return 0; + } + + int readUint16Be(uint16_t& val) { + CHECK(read((char*)&val, sizeof(val))); + val = bigEndianToNative(val); + return 0; + } + + int readUint32Be(uint32_t& val) { + CHECK(read((char*)&val, sizeof(val))); + val = bigEndianToNative(val); + return 0; + } + + int readUint64Be(uint64_t& val) { + CHECK(read((char*)&val, sizeof(val))); + val = bigEndianToNative(val); + return 0; + } + + int read(char* data, size_t size) { + size_t n = stream_.readBytes(data, size); + if (n != size) { + return Error::IO; + } + return 0; + } + +private: + Stream& stream_; +}; + +class EncodingStream { +public: + explicit EncodingStream(Print& stream) : + stream_(stream) { + } + + int writeUint8(uint8_t val) { + CHECK(write((const char*)&val, sizeof(val))); + return 0; + } + + int writeUint16Be(uint16_t val) { + val = nativeToBigEndian(val); + CHECK(write((const char*)&val, sizeof(val))); + return 0; + } + + int writeUint32Be(uint32_t val) { + val = nativeToBigEndian(val); + CHECK(write((const char*)&val, sizeof(val))); + return 0; + } + + int writeUint64Be(uint64_t val) { + val = nativeToBigEndian(val); + CHECK(write((const char*)&val, sizeof(val))); + return 0; + } + + int writeFloatBe(float val) { + uint32_t v; + static_assert(sizeof(v) == sizeof(val)); + std::memcpy(&v, &val, sizeof(val)); + v = nativeToBigEndian(v); + CHECK(write((const char*)&v, sizeof(v))); + return 0; + } + + int writeDoubleBe(double val) { + uint64_t v; + static_assert(sizeof(v) == sizeof(val)); + std::memcpy(&v, &val, sizeof(val)); + v = nativeToBigEndian(v); + CHECK(write((const char*)&v, sizeof(v))); + return 0; + } + + int write(const char* data, size_t size) { + size_t n = stream_.write((const uint8_t*)data, size); + if (n != size) { + int err = stream_.getWriteError(); + return (err < 0) ? err : Error::IO; + } + return 0; + } + +private: + Print& stream_; +}; + +struct CborHead { + uint64_t arg; + int type; + int detail; +}; + +int readAndAppendToString(DecodingStream& stream, size_t size, String& str) { + if (!str.reserve(str.length() + size)) { + return Error::NO_MEMORY; + } + char buf[128]; + while (size > 0) { + size_t n = std::min(size, sizeof(buf)); + CHECK(stream.read(buf, n)); + str.concat(buf, n); + size -= n; + } + return 0; +} + +int readCborHead(DecodingStream& stream, CborHead& head) { + uint8_t b; + CHECK(stream.readUint8(b)); + head.type = b >> 5; + head.detail = b & 0x1f; + if (head.detail < 24) { + head.arg = head.detail; + } else { + switch (head.detail) { + case 24: { // 1-byte argument + uint8_t v; + CHECK(stream.readUint8(v)); + head.arg = v; + break; + } + case 25: { // 2-byte argument + uint16_t v; + CHECK(stream.readUint16Be(v)); + head.arg = v; + break; + } + case 26: { // 4-byte argument + uint32_t v; + CHECK(stream.readUint32Be(v)); + head.arg = v; + break; + } + case 27: { // 8-byte argument + CHECK(stream.readUint64Be(head.arg)); + break; + } + case 31: { // Indefinite length indicator or stop code + if (head.type == 0 /* Unsigned integer */ || head.type == 1 /* Negative integer */ || head.type == 6 /* Tagged item */) { + return Error::BAD_DATA; + } + head.arg = 0; + break; + } + default: // Reserved (28-30) + return Error::BAD_DATA; + } + } + return 0; +} + +int writeCborHeadWithArgument(EncodingStream& stream, int type, uint64_t arg) { + type <<= 5; + if (arg < 24) { + CHECK(stream.writeUint8(arg | type)); + } else if (arg <= 0xff) { + CHECK(stream.writeUint8(24 /* 1-byte argument */ | type)); + CHECK(stream.writeUint8(arg)); + } else if (arg <= 0xffff) { + CHECK(stream.writeUint8(25 /* 2-byte argument */ | type)); + CHECK(stream.writeUint16Be(arg)); + } else if (arg <= 0xffffffffu) { + CHECK(stream.writeUint8(26 /* 4-byte argument */ | type)); + CHECK(stream.writeUint32Be(arg)); + } else { + CHECK(stream.writeUint8(27 /* 8-byte argument */ | type)); + CHECK(stream.writeUint64Be(arg)); + } + return 0; +} + +int writeCborUnsignedInteger(EncodingStream& stream, uint64_t val) { + CHECK(writeCborHeadWithArgument(stream, 0 /* Unsigned integer */, val)); + return 0; +} + +int writeCborSignedInteger(EncodingStream& stream, int64_t val) { + if (val < 0) { + val = -(val + 1); + CHECK(writeCborHeadWithArgument(stream, 1 /* Negative integer */, val)); + } else { + CHECK(writeCborHeadWithArgument(stream, 0 /* Unsigned integer */, val)); + } + return 0; +} + +int readCborString(DecodingStream& stream, const CborHead& head, String& str) { + String s; + if (head.detail == 31 /* Indefinite length */) { + for (;;) { + CborHead h; + CHECK(readCborHead(stream, h)); + if (h.type == 7 /* Misc. items */ && h.detail == 31 /* Stop code */) { + break; + } + if (h.type != 3 /* Text string */ || h.detail == 31 /* Indefinite length */) { // Chunks of indefinite length are not permitted + return Error::BAD_DATA; + } + if (h.arg > std::numeric_limits::max()) { + return Error::OUT_OF_RANGE; + } + CHECK(readAndAppendToString(stream, h.arg, s)); + } + } else { + if (head.arg > std::numeric_limits::max()) { + return Error::OUT_OF_RANGE; + } + CHECK(readAndAppendToString(stream, head.arg, s)); + } + str = std::move(s); + return 0; +} + +int writeCborString(EncodingStream& stream, const String& str) { + CHECK(writeCborHeadWithArgument(stream, 3 /* Text string */, str.length())); + CHECK(stream.write(str.c_str(), str.length())); + return 0; +} + +int encodeToCbor(EncodingStream& stream, const Variant& var) { + switch (var.type()) { + case Variant::NULL_: { + CHECK(stream.writeUint8(0xf6 /* null */)); // See RFC 8949, Appendix B + break; + } + case Variant::BOOL: { + auto v = var.value(); + CHECK(stream.writeUint8(v ? 0xf5 /* true */ : 0xf4 /* false */)); + break; + } + case Variant::INT: { + CHECK(writeCborSignedInteger(stream, var.value())); + break; + } + case Variant::UINT: { + CHECK(writeCborUnsignedInteger(stream, var.value())); + break; + } + case Variant::INT64: { + CHECK(writeCborSignedInteger(stream, var.value())); + break; + } + case Variant::UINT64: { + CHECK(writeCborUnsignedInteger(stream, var.value())); + break; + } + case Variant::DOUBLE: { + double d = var.value(); + float f = d; + if (f == d) { + // Encoding with a smaller precision than that of float is not supported + CHECK(stream.writeUint8(0xfa /* Single-precision */)); + CHECK(stream.writeFloatBe(f)); + } else { + CHECK(stream.writeUint8(0xfb /* Double-precision */)); + CHECK(stream.writeDoubleBe(d)); + } + break; + } + case Variant::STRING: { + CHECK(writeCborString(stream, var.value())); + break; + } + case Variant::ARRAY: { + auto& arr = var.value(); + CHECK(writeCborHeadWithArgument(stream, 4 /* Array */, arr.size())); + for (auto& v: arr) { + CHECK(encodeToCbor(stream, v)); + } + break; + } + case Variant::MAP: { + auto& entries = var.value().entries(); + CHECK(writeCborHeadWithArgument(stream, 5 /* Map */, entries.size())); + for (auto& e: entries) { + CHECK(writeCborString(stream, e.first)); + CHECK(encodeToCbor(stream, e.second)); + } + break; + } + default: // Unreachable + return Error::INTERNAL; + } + return 0; +} + +int decodeFromCbor(DecodingStream& stream, const CborHead& head, Variant& var) { + switch (head.type) { + case 0: { // Unsigned integer + if (head.arg <= std::numeric_limits::max()) { + var = (unsigned)head.arg; // 32-bit + } else { + var = head.arg; // 64-bit + } + break; + } + case 1: { // Negative integer + if (head.arg > (uint64_t)std::numeric_limits::max()) { + return Error::OUT_OF_RANGE; + } + int64_t v = -(int64_t)head.arg - 1; + if (v >= std::numeric_limits::min()) { + var = (int)v; // 32-bit + } else { + var = v; // 64-bit + } + break; + } + case 2: { // Byte string + return Error::NOT_SUPPORTED; // Not supported + } + case 3: { // Text string + String s; + CHECK(readCborString(stream, head, s)); + var = std::move(s); + break; + } + case 4: { // Array + VariantArray arr; + int len = -1; + if (head.detail != 31 /* Indefinite length */) { + if (head.arg > (uint64_t)std::numeric_limits::max()) { + return Error::OUT_OF_RANGE; + } + len = head.arg; + if (!arr.reserve(len)) { + return Error::NO_MEMORY; + } + } + for (;;) { + if (len >= 0 && arr.size() == len) { + break; + } + CborHead h; + CHECK(readCborHead(stream, h)); + if (h.type == 7 /* Misc. items */ && h.detail == 31 /* Stop code */) { + if (len >= 0) { + return Error::BAD_DATA; // Unexpected stop code + } + break; + } + Variant v; + CHECK(decodeFromCbor(stream, h, v)); + if (!arr.append(std::move(v))) { + return Error::NO_MEMORY; + } + } + var = std::move(arr); + break; + } + case 5: { // Map + VariantMap map; + int len = -1; + if (head.detail != 31 /* Indefinite length */) { + if (head.arg > (uint64_t)std::numeric_limits::max()) { + return Error::OUT_OF_RANGE; + } + len = head.arg; + if (!map.reserve(len)) { + return Error::NO_MEMORY; + } + } + for (;;) { + if (len >= 0 && map.size() == len) { + break; + } + CborHead h; + CHECK(readCborHead(stream, h)); + if (h.type == 7 /* Misc. items */ && h.detail == 31 /* Stop code */) { + if (len >= 0) { + return Error::BAD_DATA; // Unexpected stop code + } + break; + } + if (h.type != 3 /* Text string */) { + return Error::NOT_SUPPORTED; // Non-string keys are not supported + } + String k; + CHECK(readCborString(stream, h, k)); + Variant v; + CHECK(readCborHead(stream, h)); + CHECK(decodeFromCbor(stream, h, v)); + if (!map.set(std::move(k), std::move(v))) { + return Error::NO_MEMORY; + } + } + var = std::move(map); + break; + } + case 6: { // Tagged item + // Skip all tags + CborHead h; + do { + CHECK(readCborHead(stream, h)); + } while (h.type == 6 /* Tagged item */); + CHECK(decodeFromCbor(stream, h, var)); + break; + } + case 7: { // Misc. items + switch (head.detail) { + case 20: { // false + var = false; + break; + } + case 21: { // true + var = true; + break; + } + case 22: { // null + var = Variant(); + break; + } + case 25: { // Half-precision + // This code was taken from RFC 8949, Appendix D + uint16_t half = head.arg; + unsigned exp = (half >> 10) & 0x1f; + unsigned mant = half & 0x03ff; + double val = 0; + if (exp == 0) { + val = std::ldexp(mant, -24); + } else if (exp != 31) { + val = std::ldexp(mant + 1024, exp - 25); + } else { + val = (mant == 0) ? INFINITY : NAN; + } + if (half & 0x8000) { + val = -val; + } + var = val; + break; + } + case 26: { // Single-precision + uint32_t v = head.arg; + float val; + static_assert(sizeof(val) == sizeof(v)); + std::memcpy(&val, &v, sizeof(v)); + var = val; + break; + } + case 27: { // Double-precision + double val; + static_assert(sizeof(val) == sizeof(head.arg)); + std::memcpy(&val, &head.arg, sizeof(head.arg)); + var = val; + break; + } + default: + if ((head.detail >= 28 && head.detail <= 31) || // Reserved (28-30) or unexpected stop code (31) + (head.detail == 24 && head.arg < 32)) { // Invalid simple value + return Error::BAD_DATA; + } + return Error::NOT_SUPPORTED; // Unassigned simple value (0-19, 32-255) or undefined (23) + } + break; + } + default: // Unreachable + return Error::INTERNAL; + } + return 0; +} + +int decodeFromJson(const JSONValue& val, Variant& var) { + switch (val.type()) { + case JSONType::JSON_TYPE_INVALID: { + return Error::INVALID_ARGUMENT; + } + case JSONType::JSON_TYPE_NULL: { + var = Variant(); + break; + } + case JSONType::JSON_TYPE_BOOL: { + var = val.toBool(); + break; + } + case JSONType::JSON_TYPE_NUMBER: { + // Internally, JSONValue stores a numeric value as a pointer to its original string representation + // so conversion to a string is cheap + JSONString s = val.toString(); + // Try parsing as int + int num = 0; + auto r = detail::from_chars(s.data(), s.data() + s.size(), num); + if (r.ec != std::errc() || r.ptr != s.data() + s.size()) { + // Parse as double + double num = 0; + r = detail::from_chars(s.data(), s.data() + s.size(), num); + if (r.ec != std::errc() || r.ptr != s.data() + s.size()) { + return false; + } + var = num; + } else { + var = num; + } + break; + } + case JSONType::JSON_TYPE_STRING: { + JSONString jsonStr = val.toString(); + String s(jsonStr); + if (s.length() != jsonStr.size()) { + return Error::NO_MEMORY; + } + var = std::move(s); + break; + } + case JSONType::JSON_TYPE_ARRAY: { + JSONArrayIterator it(val); + auto& arr = var.asArray(); + if (!arr.reserve(it.count())) { + return Error::NO_MEMORY; + } + while (it.next()) { + Variant v; + CHECK(decodeFromJson(it.value(), v)); + arr.append(std::move(v)); + } + break; + } + case JSONType::JSON_TYPE_OBJECT: { + JSONObjectIterator it(val); + auto& map = var.asMap(); + if (!map.reserve(it.count())) { + return Error::NO_MEMORY; + } + while (it.next()) { + JSONString jsonKey = it.name(); + String k(jsonKey); + if (k.length() != jsonKey.size()) { + return Error::NO_MEMORY; + } + Variant v; + CHECK(decodeFromJson(it.value(), v)); + map.set(std::move(k), std::move(v)); + } + break; + } + default: // Unreachable + return Error::INTERNAL; + } + return 0; +} + +} // namespace + +namespace detail { + +#if !defined(__cpp_lib_to_chars) || defined(UNIT_TEST) + +std::to_chars_result to_chars(char* first, char* last, double value) { + std::to_chars_result res; + int n = std::snprintf(first, last - first, "%g", value); + if (n < 0 || n >= last - first) { + res.ec = std::errc::value_too_large; + res.ptr = last; + } else { + res.ec = std::errc(); + res.ptr = first + n; + } + return res; +} + +std::from_chars_result from_chars(const char* first, const char* last, double& value) { + std::from_chars_result res; + if (last > first) { + char* end = nullptr; + errno = 0; + double v = strtod(first, &end); + if (errno == ERANGE) { + res.ec = std::errc::result_out_of_range; + res.ptr = end; + } else if (end == first || std::isspace((unsigned char)*first)) { + res.ec = std::errc::invalid_argument; + res.ptr = first; + } else { + res.ec = std::errc(); + res.ptr = end; + value = v; + } + } else { + res.ec = std::errc::invalid_argument; + res.ptr = first; + } + return res; +} + +#endif // !defined(__cpp_lib_to_chars) || defined(UNIT_TEST) + +} // namespace detail + +bool Variant::append(Variant val) { + auto& arr = asArray(); + if (!ensureCapacity(arr, 1)) { + return false; + } + return arr.append(std::move(val)); +} + +bool Variant::prepend(Variant val) { + auto& arr = asArray(); + if (!ensureCapacity(arr, 1)) { + return false; + } + return arr.prepend(std::move(val)); +} + +bool Variant::insertAt(int index, Variant val) { + auto& arr = asArray(); + if (!ensureCapacity(arr, 1)) { + return false; + } + if (index < 0) { + index = 0; + } else if (index > arr.size()) { + index = arr.size(); + } + return arr.insert(index, std::move(val)); +} + +void Variant::removeAt(int index) { + if (!isArray()) { + return; + } + auto& arr = value(); + if (index < 0 || index >= arr.size()) { + return; + } + arr.removeAt(index); +} + +Variant Variant::at(int index) const { + if (!isArray()) { + return Variant(); + } + auto& arr = value(); + if (index < 0 || index >= arr.size()) { + return Variant(); + } + return arr.at(index); +} + +bool Variant::set(const char* key, Variant val) { + auto& map = asMap(); + if (!ensureCapacity(map, 1)) { + return false; + } + return map.set(key, std::move(val)); +} + +bool Variant::set(const String& key, Variant val) { + auto& map = asMap(); + if (!ensureCapacity(map, 1)) { + return false; + } + return map.set(key, std::move(val)); +} + +bool Variant::set(String&& key, Variant val) { + auto& map = asMap(); + if (!ensureCapacity(map, 1)) { + return false; + } + return map.set(std::move(key), std::move(val)); +} + +bool Variant::remove(const char* key) { + if (!isMap()) { + return false; + } + return value().remove(key); +} + +bool Variant::remove(const String& key) { + if (!isMap()) { + return false; + } + return value().remove(key); +} + +Variant Variant::get(const char* key) const { + if (!isMap()) { + return Variant(); + } + return value().get(key); +} + +Variant Variant::get(const String& key) const { + if (!isMap()) { + return Variant(); + } + return value().get(key); +} + +bool Variant::has(const char* key) const { + if (!isMap()) { + return false; + } + return value().has(key); +} + +bool Variant::has(const String& key) const { + if (!isMap()) { + return false; + } + return value().has(key); +} + +int Variant::size() const { + switch (type()) { + case Type::STRING: + return value().length(); + case Type::ARRAY: + return value().size(); + case Type::MAP: + return value().size(); + default: + return 0; + } +} + +bool Variant::isEmpty() const { + switch (type()) { + case Type::NULL_: + return true; // A default-constructed Variant is empty + case Type::STRING: + return value().length() == 0; + case Type::ARRAY: + return value().isEmpty(); + case Type::MAP: + return value().isEmpty(); + default: + return false; + } +} + +String Variant::toJSON() const { + String s; + OutputStringStream stream(s); + stream.print(*this); + if (stream.getWriteError()) { + return String(); + } + return s; +} + +Variant Variant::fromJSON(const char* json) { + return fromJSON(JSONValue::parseCopy(json)); +} + +Variant Variant::fromJSON(const JSONValue& val) { + Variant v; + int r = decodeFromJson(val, v); + if (r < 0) { + return Variant(); + } + return v; +} + +int encodeToCBOR(const Variant& var, Print& stream) { + EncodingStream s(stream); + CHECK(encodeToCbor(s, var)); + return 0; +} + +int decodeFromCBOR(Variant& var, Stream& stream) { + DecodingStream s(stream); + CborHead h; + CHECK(readCborHead(s, h)); + CHECK(decodeFromCbor(s, h, var)); + return 0; +} + +} // namespace particle